Files
carbon-lang/toolchain/check/cpp/diagnostic_consumer.cpp
T
Chandler Carruth 2c9739799b Render diagnostics through the terminal library (#7683)
Diagnostic rendering was part of the data model: `Loc::FormatLocation`
and
`Loc::FormatSnippet` wrote plain text to a stream, so no capability,
color, or
terminal width could reach them.

A diagnostic is now one message with labels, contexts, and location
information
attached, and `Diagnostics::Renderer` draws it into a
`Terminal::Buffer`. The
design is in `toolchain/docs/diagnostics_rendering.md` (#7668),
authoring in
`toolchain/docs/diagnostics.md`.

Before:

```
example.carbon:6:3: error: 1 argument passed to function expecting 0 arguments
  Run0(1);
  ^~~~~~~
example.carbon:3:1: note: calling function declared here
fn Run0() {}
^~~~~~~~~~~
```

After:

```
error: 1 argument passed to function expecting 0 arguments
       ╭─┤ example.carbon:6:3
       ┆
     3 │ fn Run0() {}
       · ─────┬─────
       ·      ╰──┤ calling function declared here
       ┆
->   6 │   Run0(1);
       ·   ───────
       ╰────────────
```

`--no-diagnostic-snippets` gives a compact form, `example.carbon:6:3-9`.
File
tests use it; the rendering tests under `toolchain/diagnostics/testdata`
ask for
snippets.

C++ interop diagnostics are data now rather than text Clang drew:
`SemIR::ClangSourceLocId` names a `clang::SourceRange`, and a Clang
location is
marked as the point it names.

`StreamConsumer` moves to a `:renderer` target that `:emitter` does not
depend
on, keeping `common/terminal` out of `consumer.h`. Capabilities come
from the
file behind the error stream, so tests get plain ASCII.

`renderer_fuzzer` draws diagnostics built from fuzzer bytes with every
capability combination, since a shape the renderer doesn't expect must
never be
why the compiler dies while reporting a problem.

TODOs mark the follow-ups: line weights and crossing lines, wrapping
that keeps
a quoted type whole, and ordering diagnostics that share a byte offset.

Also: `IncorrectExtension`'s hint moves into its message, since its note
had no
location of its own, and `strip_noise` in `check_diagnostics.py` blanks
multi-line `#define`s instead of deleting them.

Assisted-by: Claude Code
2026-10-15 06:08:57 +00:00

556 lines
23 KiB
C++

// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "toolchain/check/cpp/diagnostic_consumer.h"
#include <memory>
#include <string>
#include "clang/Basic/Diagnostic.h"
#include "clang/Basic/DiagnosticSema.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInvocation.h"
#include "clang/Lex/Lexer.h"
#include "common/check.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "toolchain/check/context.h"
#include "toolchain/check/cpp/diagnostic_listener.h"
#include "toolchain/check/cpp/location.h"
#include "toolchain/diagnostics/diagnostic.h"
#include "toolchain/diagnostics/emitter.h"
#include "toolchain/sem_ir/diagnostic_loc_converter.h"
namespace Carbon::Check {
class CarbonClangDiagnosticConsumer;
// A diagnostic emitter that maps Clang source ranges to Carbon diagnostic
// locations.
class ClangLocDiagnosticEmitter
: public Diagnostics::Emitter<clang::CharSourceRange> {
public:
explicit ClangLocDiagnosticEmitter(Diagnostics::Consumer* consumer,
const clang::SourceManager* source_manager)
: Emitter(consumer), source_manager_(source_manager) {}
protected:
auto ConvertLoc(clang::CharSourceRange range, ContextFnT /*context_fn*/) const
-> Diagnostics::ConvertedLoc override {
Diagnostics::Loc result_loc;
if (source_manager_) {
result_loc = SemIR::ConvertClangRangeToLoc(*source_manager_, range);
}
return {.loc = result_loc, .last_byte_offset = -1};
}
private:
const clang::SourceManager* source_manager_;
};
// Attaches a label saying what Clang suggests be done to a range. Clang words
// these as an edit to make, so they read as an instruction rather than as a
// description of the code.
//
// TODO: Carry fix-its as data -- span, replacement, confidence -- in a form
// Carbon's own diagnostics can share, rather than wording the edit into a
// label here, and render both as a unified diff following GCC's example
// rather than Clang's. See future work in
// /toolchain/docs/diagnostics_rendering.md.
template <typename BuilderT, typename LocT>
static auto AttachFixIt(BuilderT& builder, LocT loc,
const CppDiagnosticListener::FixIt& fix_it) -> void {
if (fix_it.text.empty()) {
CARBON_DIAGNOSTIC_LABEL(CppInteropFixItRemoval, Info, "remove this");
builder.Attach(loc, CppInteropFixItRemoval);
return;
}
if (fix_it.range.getBegin() == fix_it.range.getEnd()) {
CARBON_DIAGNOSTIC_LABEL(CppInteropFixItInsertion, Info, "insert `{0}` here",
std::string);
builder.Attach(loc, CppInteropFixItInsertion, fix_it.text);
return;
}
CARBON_DIAGNOSTIC_LABEL(CppInteropFixItReplacement, Info,
"replace with `{0}`", std::string);
builder.Attach(loc, CppInteropFixItReplacement, fix_it.text);
}
// 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: {
// A note explains the diagnostic it follows and is attached to that one
// as a label. One can still lead a buffer -- a flush can land between an
// error and its trailing notes, and Clang can emit a stray note -- and
// however wrong that is of Clang, it must not be why a compiler dies
// while reporting a problem.
CARBON_DIAGNOSTIC(CppInteropStrayNote, Warning, "{0}", std::string);
return CppInteropStrayNote;
}
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;
}
}
}
// Returns whether `range` holds `loc`, which needs both to be spelled in the
// same file: two offsets from different files are unordered, and comparing them
// would report a containment that isn't one.
static auto Contains(const clang::SourceManager& source_manager,
clang::CharSourceRange range, clang::SourceLocation loc)
-> bool {
if (range.isInvalid()) {
return false;
}
CARBON_CHECK(range.isCharRange(), "Widen a token range before comparing.");
auto [file_id, offset] = source_manager.getDecomposedSpellingLoc(loc);
auto [begin_file_id, begin] =
source_manager.getDecomposedSpellingLoc(range.getBegin());
auto [end_file_id, end] =
source_manager.getDecomposedSpellingLoc(range.getEnd());
return begin_file_id == file_id && end_file_id == file_id &&
begin <= offset && offset < end;
}
// Returns whether `diag_id` is a Clang diagnostic whose text is written as
// `<what was considered>: <why it was not viable>`, with its first range
// marking the source that second half is about.
//
// These are the overload-resolution candidate notes, and each one is listed
// rather than being recognized by shape. Splitting on the first `: ` in any
// note that happens to carry a range would cut the wrong messages in the wrong
// place: a type or a path in the text supplies its own colon.
//
// TODO: Try to do this upstream instead. Clang has the structure here and
// loses it: `DiagnoseBadConversion` and its siblings know which half is the
// candidate and which is the reason, and know which argument the reason is
// about, and then format both halves into one string. A note carrying its two
// halves separately would serve Clang's own rendering as well, and would leave
// nothing here to take apart. Until then this list has to be revisited
// whenever the wording of one of these notes changes.
static auto SplitsIntoCandidateAndReason(unsigned diag_id) -> bool {
switch (diag_id) {
case clang::diag::note_ovl_candidate_arity:
case clang::diag::note_ovl_candidate_arity_one:
case clang::diag::note_ovl_candidate_bad_base_to_derived_conv:
case clang::diag::note_ovl_candidate_bad_conv:
case clang::diag::note_ovl_candidate_bad_conv_incomplete:
case clang::diag::note_ovl_candidate_bad_cvr:
case clang::diag::note_ovl_candidate_bad_cvr_this:
case clang::diag::note_ovl_candidate_bad_list_argument:
case clang::diag::note_ovl_candidate_bad_value_category:
return true;
default:
return false;
}
}
// Returns the parameter list of the declaration named at `loc`, which is what
// a note marks when it has nothing of its own to mark.
//
// Clang leaves a function's parameter-list range empty when the function
// declares no parameters, so a note about how many arguments it takes marks
// nothing even though the list is written. The list is the parentheses after
// the name, holding at most `void`, and it is where the count comes from.
//
// Returns an invalid range where those parentheses aren't right there: the name
// may be an implicit function's, which has no list at all, or be followed by
// template arguments, and reaching further would mark something that isn't the
// list.
static auto FindEmptyParameterList(const clang::SourceManager& source_manager,
const clang::LangOptions& lang_opts,
clang::SourceLocation loc)
-> clang::CharSourceRange {
if (loc.isInvalid() || loc.isMacroID()) {
return clang::CharSourceRange();
}
// `operator()`'s name holds parentheses of its own, and the parameter list
// is past them; the token after the name would be the name's `(`, not the
// list's.
clang::Token name;
if (clang::Lexer::getRawToken(loc, name, source_manager, lang_opts,
/*IgnoreWhiteSpace=*/true) ||
(name.is(clang::tok::raw_identifier) &&
name.getRawIdentifier() == "operator")) {
return clang::CharSourceRange();
}
auto open = clang::Lexer::findNextToken(loc, source_manager, lang_opts);
if (!open || !open->is(clang::tok::l_paren)) {
return clang::CharSourceRange();
}
auto close = clang::Lexer::findNextToken(open->getLocation(), source_manager,
lang_opts);
// An empty list is written as `()` or as `(void)`. The lexing here is raw, so
// `void` arrives as an identifier rather than as the keyword it is.
if (close && close->is(clang::tok::raw_identifier) &&
close->getRawIdentifier() == "void") {
close = clang::Lexer::findNextToken(close->getLocation(), source_manager,
lang_opts);
}
if (!close || !close->is(clang::tok::r_paren)) {
return clang::CharSourceRange();
}
return clang::CharSourceRange::getCharRange(open->getLocation(),
close->getEndLoc());
}
// Returns the label to attach for a Clang note, which says more about the
// diagnostic it follows rather than reporting a problem of its own.
static auto GetNoteLabel() -> const Diagnostics::LabelBase<std::string>& {
CARBON_DIAGNOSTIC_LABEL(CppInteropParseNote, Info, "{0}", std::string);
return CppInteropParseNote;
}
// Returns the label to attach for what a note says about the source it marks,
// as opposed to the declaration the note names.
static auto GetNoteReasonLabel() -> const Diagnostics::LabelBase<std::string>& {
CARBON_DIAGNOSTIC_LABEL(CppInteropParseNoteReason, Info, "{0}", std::string);
return CppInteropParseNoteReason;
}
// Attaches what `info` marks: the ranges Clang would underline, which say
// nothing the message doesn't and so only mark, and the changes it suggests,
// which explain rather than state and so carry words.
//
// A range belonging to the leading diagnostic is part of the problem; one
// belonging to a note explains it, the same as the note itself does.
//
// `to_loc` maps a Clang range onto whatever locations the builder takes.
template <typename BuilderT, typename ToLocT>
static auto AttachMarks(BuilderT& builder,
const CppDiagnosticListener::Diagnostic& info,
bool is_note, ToLocT to_loc) -> void {
for (clang::CharSourceRange range : info.ranges) {
builder.Attach(to_loc(range), is_note
? Diagnostics::LabelCategory::Info
: Diagnostics::LabelCategory::Primary);
}
for (const CppDiagnosticListener::FixIt& fix_it : info.fix_its) {
AttachFixIt(builder, to_loc(fix_it.range), fix_it);
}
}
// Attaches `note_info`, a note trailing the diagnostic being built: its words
// on the declaration it names, its reason half -- when it split into one --
// on the source that half is about, and then whatever it marks.
template <typename BuilderT, typename ToLocT>
static auto AttachNote(BuilderT& builder,
const CppDiagnosticListener::Diagnostic& note_info,
ToLocT to_loc) -> void {
builder.Attach(to_loc(note_info.location), GetNoteLabel(), note_info.message);
if (!note_info.reason.empty()) {
builder.Attach(to_loc(note_info.reason_range), GetNoteReasonLabel(),
note_info.reason);
}
AttachMarks(builder, note_info, /*is_note=*/true, to_loc);
}
// A listener that emits Clang diagnostics directly to a provided Carbon
// Diagnostics::Consumer when no Carbon Context is active.
class FallbackDiagnosticListener : public CppDiagnosticListener {
public:
explicit FallbackDiagnosticListener(
CarbonClangDiagnosticConsumer& clang_consumer,
Diagnostics::Consumer& carbon_consumer)
: CppDiagnosticListener(clang_consumer),
carbon_consumer_(&carbon_consumer) {}
auto EmitDiagnostics(llvm::ArrayRef<Diagnostic> diags) -> void override {
if (diags.empty()) {
return;
}
ClangLocDiagnosticEmitter emitter(carbon_consumer_,
diags[0].source_manager);
auto as_range = [](clang::CharSourceRange range) { return range; };
for (size_t i = 0; i != diags.size(); ++i) {
const Diagnostic& info = diags[i];
auto builder =
emitter.Build(info.location, GetDiagnostic(info.level), info.message);
AttachMarks(builder, info, /*is_note=*/false, as_range);
for (; i + 1 < diags.size() &&
diags[i + 1].level == clang::DiagnosticsEngine::Note;
++i) {
AttachNote(builder, diags[i + 1], as_range);
}
builder.Emit();
}
}
private:
Diagnostics::Consumer* carbon_consumer_;
};
// A listener that converts Clang diagnostics to Carbon diagnostics using a
// Carbon Context.
class ContextDiagnosticListener : public CppDiagnosticListener {
public:
explicit ContextDiagnosticListener(CarbonClangDiagnosticConsumer& consumer,
Context& context)
: CppDiagnosticListener(consumer), context_(&context) {}
auto EmitDiagnostics(llvm::ArrayRef<Diagnostic> diags) -> void override {
if (diags.empty()) {
return;
}
CARBON_CHECK(
context_->sem_ir().cpp_file(),
"Attempted to emit C++ diagnostics before the C++ file is set");
for (size_t i = 0; i != diags.size(); ++i) {
const Diagnostic& info = diags[i];
SemIR::ImportIRInstId import_ir_inst_id =
AddImportIRInst(context_->sem_ir(), info.location);
auto builder =
context_->emitter().Build(SemIR::LocId(import_ir_inst_id),
GetDiagnostic(info.level), info.message);
auto as_loc_id = [&](clang::CharSourceRange range) {
return SemIR::LocId(AddImportIRInst(context_->sem_ir(), range));
};
AttachMarks(builder, info, /*is_note=*/false, as_loc_id);
for (; i + 1 < diags.size() &&
diags[i + 1].level == clang::DiagnosticsEngine::Note;
++i) {
AttachNote(builder, diags[i + 1], as_loc_id);
}
// 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();
}
}
private:
Context* context_;
};
// 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, listeners 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.
class CarbonClangDiagnosticConsumer : public clang::DiagnosticConsumer {
public:
explicit CarbonClangDiagnosticConsumer(
Diagnostics::Consumer& consumer,
std::shared_ptr<clang::CompilerInvocation> invocation)
: fallback_listener_(*this, consumer),
invocation_(std::move(invocation)) {}
~CarbonClangDiagnosticConsumer() override {
CARBON_CHECK(diagnostic_infos_.empty(), "Missing flush before destruction");
CARBON_CHECK(listeners_.size() == 1,
"Diagnostic listeners were not properly popped");
}
// Pushes a listener onto the stack. Diagnostics will be forwarded to the
// innermost listener.
auto PushListener(CppDiagnosticListener* listener) -> void {
CARBON_CHECK(diagnostic_infos_.empty(),
"Missing flush before pushing listener");
CARBON_CHECK(listener);
listeners_.push_back(listener);
}
// Pops a listener from the stack.
auto PopListener(CppDiagnosticListener* listener) -> void {
CARBON_CHECK(diagnostic_infos_.empty(),
"Missing flush before popping listener");
CARBON_CHECK(!listeners_.empty() && listeners_.back() == listener,
"Popping unexpected diagnostic listener");
listeners_.pop_back();
}
// Flushes the innermost listener.
auto Flush() -> void {
if (diagnostic_infos_.empty()) {
return;
}
CARBON_CHECK(!listeners_.empty(), "No diagnostic listeners registered");
listeners_.back()->EmitDiagnostics(diagnostic_infos_);
diagnostic_infos_.clear();
}
auto HandleDiagnostic(clang::DiagnosticsEngine::Level diag_level,
const clang::Diagnostic& info) -> void override {
DiagnosticConsumer::HandleDiagnostic(diag_level, info);
if (diag_level != clang::DiagnosticsEngine::Note &&
!diagnostic_infos_.empty()) {
Flush();
}
llvm::SmallString<256> message;
info.FormatDiagnostic(message);
// What Clang would underline and what it suggests changing, kept as ranges
// rather than as text: the toolchain marks and labels them itself, so that
// a diagnostic from C++ reads like one from Carbon. The `#include` and
// macro-expansion stacks reach the location as `included from` and
// `expanded from macro defined at` steps, drawn like any other path a
// location was reached by.
const clang::SourceManager* source_manager =
info.hasSourceManager() ? &info.getSourceManager() : nullptr;
// Clang's token ranges reach to the start of their last token rather than
// past it, so they are widened here. Everything downstream then measures a
// range by subtracting its ends, and an empty one is an insertion point
// rather than a range that happens to name one token.
auto as_char_range = [&](clang::CharSourceRange range) {
if (!source_manager) {
return range;
}
return clang::Lexer::getAsCharRange(range, *source_manager,
invocation_->getLangOpts());
};
llvm::SmallVector<clang::CharSourceRange, 2> ranges;
for (clang::CharSourceRange range : info.getRanges()) {
ranges.push_back(as_char_range(range));
}
llvm::SmallVector<CppDiagnosticListener::FixIt, 0> fix_its;
for (const clang::FixItHint& hint : info.getFixItHints()) {
// A hint copying code from elsewhere carries it in `InsertFromRange`,
// which nothing here reads; presented as the removal its empty
// `CodeToInsert` looks like, it would instruct the opposite edit.
// TODO: Map such a hint to an insertion of the text `InsertFromRange`
// names.
if (hint.isNull() || hint.InsertFromRange.isValid()) {
continue;
}
// Widening fails for a token range ending inside a macro; Clang's own
// rendering drops such a fix-it too.
clang::CharSourceRange range = as_char_range(hint.RemoveRange);
if (range.isInvalid()) {
continue;
}
fix_its.push_back({.range = range, .text = hint.CodeToInsert});
}
// A Clang location names a point, and is marked as the one column Clang's
// own caret marks. How far it reaches is said only by a range attached
// alongside it, and a location can name the middle of a token, as one in
// a format string does.
clang::SourceLocation begin = info.getLocation();
clang::CharSourceRange location =
clang::CharSourceRange::getCharRange(begin, begin);
// Clang draws a range holding the caret as part of the caret's own mark
// rather than beside it -- the `~~~~` of a `^~~~~` -- so such a range is
// what the message is about rather than something else it points at. The
// rest stay marks of their own.
//
// TODO: Also mark where the caret is within the range, as Clang's
// `~~~^~~~` does. That needs the renderer to be able to mark a point
// inside a range it underlines.
if (source_manager && begin.isValid()) {
auto* found = llvm::find_if(ranges, [&](clang::CharSourceRange range) {
return Contains(*source_manager, range, begin);
});
if (found != ranges.end()) {
location = *found;
ranges.erase(found);
}
}
if (!source_manager) {
// Without a source manager this is an error from early in the frontend,
// and there is nothing to mark.
CARBON_CHECK(info.getLocation().isInvalid());
ranges.clear();
fix_its.clear();
}
// A note that says one thing about the declaration it names and another
// about the source it marks becomes a label on each, rather than one
// sentence hung off the name. Which notes are written that way is stated
// by `SplitsIntoCandidateAndReason` rather than guessed at from the text.
llvm::StringRef head = message.str();
llvm::StringRef reason;
clang::CharSourceRange reason_range;
if (source_manager && SplitsIntoCandidateAndReason(info.getID()) &&
!ranges.empty()) {
if (ranges.front().isInvalid()) {
ranges.front() = FindEmptyParameterList(
*source_manager, invocation_->getLangOpts(), begin);
}
auto [before, after] = head.split(": ");
if (ranges.front().isValid() && !after.empty()) {
head = before;
reason = after;
reason_range = ranges.front();
ranges.erase(ranges.begin());
}
}
llvm::erase_if(
ranges, [](clang::CharSourceRange range) { return range.isInvalid(); });
diagnostic_infos_.push_back({.level = diag_level,
.location = location,
.source_manager = source_manager,
.message = head.str(),
.reason = reason.str(),
.reason_range = reason_range,
.ranges = std::move(ranges),
.fix_its = std::move(fix_its)});
}
private:
llvm::SmallVector<CppDiagnosticListener::Diagnostic> diagnostic_infos_;
llvm::SmallVector<CppDiagnosticListener*, 2> listeners_;
FallbackDiagnosticListener fallback_listener_;
std::shared_ptr<clang::CompilerInvocation> invocation_;
};
CppDiagnosticListener::CppDiagnosticListener(
CarbonClangDiagnosticConsumer& consumer)
: consumer_(&consumer) {
consumer_->PushListener(this);
}
CppDiagnosticListener::~CppDiagnosticListener() {
consumer_->PopListener(this);
}
auto MakeDiagnosticConsumer(
Diagnostics::Consumer& consumer,
std::shared_ptr<clang::CompilerInvocation> invocation)
-> std::unique_ptr<clang::DiagnosticConsumer> {
return std::make_unique<CarbonClangDiagnosticConsumer>(consumer,
std::move(invocation));
}
auto FlushDiagnosticConsumer(clang::DiagnosticConsumer& consumer) -> void {
static_cast<CarbonClangDiagnosticConsumer&>(consumer).Flush();
}
auto MakeContextDiagnosticListener(clang::DiagnosticConsumer& consumer,
Context& context)
-> std::unique_ptr<CppDiagnosticListener> {
auto* clang_consumer = static_cast<CarbonClangDiagnosticConsumer*>(&consumer);
auto listener =
std::make_unique<ContextDiagnosticListener>(*clang_consumer, context);
context.emitter().AddFlushFn([clang_consumer] { clang_consumer->Flush(); });
return listener;
}
} // namespace Carbon::Check