Split up C++ diagnostic emitter and factor it out. (#7558)

Split the diagnostic emitter into a separate emitter (regietered with
Clang) and listener (registered with the emitter). The purpose of this
split is to make the Clang emitter not depend on the `Check::Context`,
so that we can use it, and hence the same Clang instance, with multiple
`Check::Context`s. A fallback listener is registered to collect and emit
any diagnostics produced while we don't have a `Check::Context`
registered with the emitter.

Assisted-by: Gemini via Antigravity
This commit is contained in:
Richard Smith
2026-07-24 20:14:50 +00:00
committed by GitHub
parent 48a03e6a1c
commit 39916ad2ca
11 changed files with 443 additions and 219 deletions
+22 -209
View File
@@ -9,6 +9,7 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/Basic/DiagnosticParse.h"
#include "clang/Basic/FileManager.h"
#include "clang/CodeGen/ModuleBuilder.h"
#include "clang/Frontend/CompilerInstance.h"
@@ -30,6 +31,8 @@
#include "toolchain/base/kind_switch.h"
#include "toolchain/check/context.h"
#include "toolchain/check/cpp/access.h"
#include "toolchain/check/cpp/diagnostic_consumer.h"
#include "toolchain/check/cpp/diagnostic_listener.h"
#include "toolchain/check/cpp/export.h"
#include "toolchain/check/cpp/import.h"
#include "toolchain/check/cpp/location.h"
@@ -107,204 +110,8 @@ static auto GenerateCppIncludesHeaderCode(
return code_stream.TakeStr();
}
// Adds the given source location and an `ImportIRInst` referring to it in
// `ImportIRId::Cpp`.
static auto AddImportIRInst(SemIR::File& file,
clang::SourceLocation clang_source_loc)
-> SemIR::ImportIRInstId {
SemIR::ClangSourceLocId clang_source_loc_id =
file.clang_source_locs().Add(clang_source_loc);
return file.import_ir_insts().Add(SemIR::ImportIRInst(clang_source_loc_id));
}
namespace {
// Used to convert Clang diagnostics to Carbon diagnostics.
//
// Handling of Clang notes is a little subtle: as far as Clang is concerned,
// notes are separate diagnostics, not connected to the error or warning that
// precedes them. But in Carbon's diagnostics system, notes are part of the
// enclosing diagnostic. To handle this, we buffer Clang diagnostics until we
// reach a point where we know we're not in the middle of a diagnostic, and then
// emit a diagnostic along with all of its notes. This is triggered when adding
// or removing a Carbon context note, which could otherwise get attached to the
// wrong C++ diagnostics, and at the end of the Carbon program.
class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
public:
// Creates an instance with the location that triggers calling Clang. The
// `context` is not stored here, and the diagnostics consumer is expected to
// outlive it.
explicit CarbonClangDiagnosticConsumer(
Context& context, std::shared_ptr<clang::CompilerInvocation> invocation)
: sem_ir_(&context.sem_ir()),
emitter_(&context.emitter()),
invocation_(std::move(invocation)) {
emitter_->AddFlushFn([this] { EmitDiagnostics(); });
}
~CarbonClangDiagnosticConsumer() override {
// Do not inspect `emitter_` here; it's typically destroyed before the
// consumer is.
// TODO: If Clang produces diagnostics after check finishes, they'll get
// added to the list of pending diagnostics and never emitted.
CARBON_CHECK(diagnostic_infos_.empty(),
"Missing flush before destroying diagnostic consumer");
}
// Generates a Carbon warning for each Clang warning and a Carbon error for
// each Clang error or fatal.
auto HandleDiagnostic(clang::DiagnosticsEngine::Level diag_level,
const clang::Diagnostic& info) -> void override {
DiagnosticConsumer::HandleDiagnostic(diag_level, info);
SemIR::ImportIRInstId clang_import_ir_inst_id =
AddImportIRInst(*sem_ir_, info.getLocation());
llvm::SmallString<256> message;
info.FormatDiagnostic(message);
// Render a code snippet including any highlighted ranges and fixit hints.
// TODO: Also include the #include stack and macro expansion stack in the
// diagnostic output in some way.
RawStringOstream snippet_stream;
if (!info.hasSourceManager()) {
// If we don't have a source manager, this is an error from early in the
// frontend. Don't produce a snippet.
CARBON_CHECK(info.getLocation().isInvalid());
} else {
CodeContextRenderer(snippet_stream, invocation_->getLangOpts(),
invocation_->getDiagnosticOpts())
.emitDiagnostic(
clang::FullSourceLoc(info.getLocation(), info.getSourceManager()),
diag_level, message, info.getRanges(), info.getFixItHints());
}
diagnostic_infos_.push_back({.level = diag_level,
.import_ir_inst_id = clang_import_ir_inst_id,
.message = message.str().str(),
.snippet = snippet_stream.TakeStr()});
}
// Returns the diagnostic to use for a given Clang diagnostic level.
static auto GetDiagnostic(clang::DiagnosticsEngine::Level level)
-> const Diagnostics::DiagnosticBase<std::string>& {
switch (level) {
case clang::DiagnosticsEngine::Ignored: {
CARBON_FATAL("Emitting an ignored diagnostic");
break;
}
case clang::DiagnosticsEngine::Note: {
CARBON_DIAGNOSTIC(CppInteropParseNote, Note, "{0}", std::string);
return CppInteropParseNote;
}
case clang::DiagnosticsEngine::Remark:
case clang::DiagnosticsEngine::Warning: {
// TODO: Add a distinct Remark level to Carbon diagnostics, and stop
// mapping remarks to warnings.
CARBON_DIAGNOSTIC(CppInteropParseWarning, Warning, "{0}", std::string);
return CppInteropParseWarning;
}
case clang::DiagnosticsEngine::Error:
case clang::DiagnosticsEngine::Fatal: {
CARBON_DIAGNOSTIC(CppInteropParseError, Error, "{0}", std::string);
return CppInteropParseError;
}
}
}
// Outputs Carbon diagnostics based on the collected Clang diagnostics. Must
// be called after the AST is set in the context.
auto EmitDiagnostics() -> void {
CARBON_CHECK(
sem_ir_->cpp_file(),
"Attempted to emit C++ diagnostics before the C++ file is set");
for (size_t i = 0; i != diagnostic_infos_.size(); ++i) {
const ClangDiagnosticInfo& info = diagnostic_infos_[i];
auto builder = emitter_->Build(SemIR::LocId(info.import_ir_inst_id),
GetDiagnostic(info.level), info.message);
builder.OverrideSnippet(info.snippet);
for (; i + 1 < diagnostic_infos_.size() &&
diagnostic_infos_[i + 1].level == clang::DiagnosticsEngine::Note;
++i) {
const ClangDiagnosticInfo& note_info = diagnostic_infos_[i + 1];
builder
.Note(SemIR::LocId(note_info.import_ir_inst_id),
GetDiagnostic(note_info.level), note_info.message)
.OverrideSnippet(note_info.snippet);
}
// TODO: This will apply all current Carbon annotation functions. We
// should instead track how Clang's context notes and Carbon's annotation
// functions are interleaved, and interleave the notes in the same order.
builder.Emit();
}
diagnostic_infos_.clear();
}
private:
// A diagnostics renderer based on clang's TextDiagnostic that captures just
// the code context (the snippet).
class CodeContextRenderer : public clang::TextDiagnostic {
protected:
using TextDiagnostic::TextDiagnostic;
void emitDiagnosticMessage(
clang::FullSourceLoc /*loc*/, clang::PresumedLoc /*ploc*/,
clang::DiagnosticsEngine::Level /*level*/, llvm::StringRef /*message*/,
llvm::ArrayRef<clang::CharSourceRange> /*ranges*/,
clang::DiagOrStoredDiag /*info*/) override {}
void emitDiagnosticLoc(
clang::FullSourceLoc /*loc*/, clang::PresumedLoc /*ploc*/,
clang::DiagnosticsEngine::Level /*level*/,
llvm::ArrayRef<clang::CharSourceRange> /*ranges*/) override {}
// emitCodeContext is inherited from clang::TextDiagnostic.
void emitIncludeLocation(clang::FullSourceLoc /*loc*/,
clang::PresumedLoc /*ploc*/) override {}
void emitImportLocation(clang::FullSourceLoc /*loc*/,
clang::PresumedLoc /*ploc*/,
llvm::StringRef /*module_name*/) override {}
void emitBuildingModuleLocation(clang::FullSourceLoc /*loc*/,
clang::PresumedLoc /*ploc*/,
llvm::StringRef /*module_name*/) override {}
// beginDiagnostic and endDiagnostic are inherited from
// clang::TextDiagnostic in case it wants to do any setup / teardown work.
};
// Information on a Clang diagnostic that can be converted to a Carbon
// diagnostic.
struct ClangDiagnosticInfo {
// The Clang diagnostic level.
clang::DiagnosticsEngine::Level level;
// The ID of the ImportIR instruction referring to the Clang source
// location.
SemIR::ImportIRInstId import_ir_inst_id;
// The Clang diagnostic textual message.
std::string message;
// The code snippet produced by clang.
std::string snippet;
};
// The Carbon file that this C++ compilation is attached to.
SemIR::File* sem_ir_;
// The diagnostic emitter that we're emitting diagnostics into.
DiagnosticEmitterBase* emitter_;
// The compiler invocation that is producing the diagnostics.
std::shared_ptr<clang::CompilerInvocation> invocation_;
// Collects the information for all Clang diagnostics to be converted to
// Carbon diagnostics after the context has been initialized with the Clang
// AST.
llvm::SmallVector<ClangDiagnosticInfo> diagnostic_infos_;
};
// A wrapper around a clang::CompilerInvocation that allows us to make a shallow
// copy of most of the invocation and only make a deep copy of the parts that we
// want to change.
@@ -398,11 +205,7 @@ class CarbonExternalASTSource : public SemIR::ReadOnlyASTSource {
clang_source_loc = code_synthesis_contexts.back().PointOfInstantiation;
}
// TODO: Refactor with AddImportIRInst in import.cpp.
SemIR::ClangSourceLocId clang_source_loc_id =
context_->sem_ir().clang_source_locs().Add(clang_source_loc);
return context_->import_ir_insts().Add(
SemIR::ImportIRInst(clang_source_loc_id));
return AddImportIRInst(context_->sem_ir(), clang_source_loc);
}
// For LLVM RTTI.
@@ -813,7 +616,11 @@ namespace {
// from a set of Cpp imports.
class GenerateASTAction : public clang::ASTFrontendAction {
public:
explicit GenerateASTAction(Context& context) : context_(&context) {}
explicit GenerateASTAction(
Context& context,
std::unique_ptr<CppDiagnosticListener> diagnostic_listener)
: context_(&context),
diagnostic_listener_(std::move(diagnostic_listener)) {}
protected:
auto CreateASTConsumer(clang::CompilerInstance& clang_instance,
@@ -858,8 +665,10 @@ class GenerateASTAction : public clang::ASTFrontendAction {
clang_instance.getPreprocessor().EnterMainSourceFile();
parser.Initialize();
context_->set_cpp_context(
std::make_unique<CppContext>(clang_instance, std::move(parser_ptr)));
auto cpp_context =
std::make_unique<CppContext>(clang_instance, std::move(parser_ptr));
cpp_context->set_diagnostic_listener(std::move(diagnostic_listener_));
context_->set_cpp_context(std::move(cpp_context));
if (auto* source = clang_instance.getASTContext().getExternalSource()) {
source->StartTranslationUnit(&clang_instance.getASTConsumer());
@@ -872,6 +681,7 @@ class GenerateASTAction : public clang::ASTFrontendAction {
private:
Context* context_;
std::unique_ptr<CppDiagnosticListener> diagnostic_listener_;
};
} // namespace
@@ -891,11 +701,14 @@ auto GenerateAst(Context& context,
// Ask Clang to not leak memory.
invocation->getFrontendOpts().DisableFree = false;
auto diagnostic_consumer =
MakeDiagnosticConsumer(context.emitter().consumer(), invocation);
auto* diagnostic_consumer_ptr = diagnostic_consumer.get();
// Build a diagnostics engine.
llvm::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diags(
clang::CompilerInstance::createDiagnostics(
*fs, invocation->getDiagnosticOpts(),
new CarbonClangDiagnosticConsumer(context, invocation),
*fs, invocation->getDiagnosticOpts(), diagnostic_consumer.release(),
/*ShouldOwnClient=*/true));
// Extract the input from the frontend invocation and make sure it makes
@@ -936,7 +749,8 @@ auto GenerateAst(Context& context,
return false;
}
GenerateASTAction action(context);
GenerateASTAction action(context, MakeContextDiagnosticListener(
*diagnostic_consumer_ptr, context));
if (!action.BeginSourceFile(clang_instance, inputs[0])) {
return false;
}
@@ -1017,6 +831,7 @@ auto FinishAst(Context& context) -> void {
}
context.cpp_context()->sema().ActOnEndOfTranslationUnit();
context.emitter().Flush();
// Remove the `CarbonExternalASTSource` installed in `GenerateAst` and
// replace it with a `ReadOnlyASTSource`. This is necessary because
@@ -1034,8 +849,6 @@ auto FinishAst(Context& context) -> void {
// We don't call FrontendAction::EndSourceFile, because that destroys the AST.
context.set_cpp_context(nullptr);
context.emitter().Flush();
}
} // namespace Carbon::Check