mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-11-15 13:58:29 +00:00
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
288 lines
11 KiB
C++
288 lines
11 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/diagnostics/emitter.h"
|
|
|
|
#include <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <string>
|
|
|
|
#include "llvm/ADT/StringRef.h"
|
|
#include "toolchain/diagnostics/mocks.h"
|
|
|
|
namespace Carbon::Testing {
|
|
namespace {
|
|
|
|
using testing::ElementsAre;
|
|
using testing::IsEmpty;
|
|
|
|
class FakeEmitter : public Diagnostics::Emitter<int> {
|
|
public:
|
|
using Emitter::Emitter;
|
|
|
|
protected:
|
|
auto ConvertLoc(int n, ContextFnT /*context_fn*/) const
|
|
-> Diagnostics::ConvertedLoc override {
|
|
return {.loc = {.line_number = 1, .column_number = n},
|
|
.last_byte_offset = -1};
|
|
}
|
|
};
|
|
|
|
class EmitterTest : public ::testing::Test {
|
|
public:
|
|
EmitterTest() : emitter_(&consumer_) {}
|
|
|
|
Testing::MockDiagnosticConsumer consumer_;
|
|
FakeEmitter emitter_;
|
|
};
|
|
|
|
TEST_F(EmitterTest, EmitSimpleError) {
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Error, "simple error");
|
|
EXPECT_CALL(consumer_, HandleDiagnostic(IsSingleDiagnostic(
|
|
Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 1, "simple error")));
|
|
EXPECT_CALL(consumer_, HandleDiagnostic(IsSingleDiagnostic(
|
|
Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 2, "simple error")));
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
emitter_.Emit(2, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitSimpleWarning) {
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(consumer_,
|
|
HandleDiagnostic(IsSingleDiagnostic(
|
|
Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1, "simple warning")));
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitOneArgDiagnostic) {
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Error, "arg: `{0}`", std::string);
|
|
EXPECT_CALL(consumer_, HandleDiagnostic(IsSingleDiagnostic(
|
|
Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 1, "arg: `str`")));
|
|
emitter_.Emit(1, TestDiagnostic, "str");
|
|
}
|
|
|
|
TEST_F(EmitterTest, AttachLabel) {
|
|
// The label has arguments and a format of its own, both validated.
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Error, "expected {0}, found {1}",
|
|
std::string, std::string);
|
|
CARBON_DIAGNOSTIC_LABEL(TestLabel, Primary, "this is {0}", std::string);
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Error,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 1,
|
|
"expected i32, found String"),
|
|
IsEmpty(),
|
|
ElementsAre(IsDiagnosticLabel(Diagnostics::LabelCategory::Primary, 1,
|
|
2, "this is String")))));
|
|
emitter_.Build(1, TestDiagnostic, "i32", "String")
|
|
.Attach(2, TestLabel, "String")
|
|
.Emit();
|
|
}
|
|
|
|
TEST_F(EmitterTest, AttachRangeWithNoText) {
|
|
// A label with nothing to say marks its range and no more, which is how a
|
|
// diagnostic points at the code its message is about.
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Error, "simple error");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Error,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 1, "simple error"),
|
|
IsEmpty(),
|
|
ElementsAre(IsDiagnosticLabel(Diagnostics::LabelCategory::Primary, 1,
|
|
2, "")))));
|
|
emitter_.Build(1, TestDiagnostic).Attach(2).Emit();
|
|
}
|
|
|
|
TEST_F(EmitterTest, AttachRangeWithNoTextAsInfo) {
|
|
// A range that only explains the problem can say nothing about itself too,
|
|
// which is what the category on the wordless `Attach` is for: there is no
|
|
// phrase to declare, so there is nowhere else to say what the range is.
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Error, "simple error");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Error,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Error, 1, 1, "simple error"),
|
|
IsEmpty(),
|
|
ElementsAre(
|
|
IsDiagnosticLabel(Diagnostics::LabelCategory::Info, 1, 2, "")))));
|
|
emitter_.Build(1, TestDiagnostic)
|
|
.Attach(2, Diagnostics::LabelCategory::Info)
|
|
.Emit();
|
|
}
|
|
|
|
TEST_F(EmitterTest, AttachInfo) {
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
CARBON_DIAGNOSTIC_LABEL(TestInfo, Info, "note");
|
|
EXPECT_CALL(consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
IsEmpty(),
|
|
ElementsAre(IsDiagnosticLabel(
|
|
Diagnostics::LabelCategory::Info, 1, 2, "note")))));
|
|
emitter_.Build(1, TestDiagnostic).Attach(2, TestInfo).Emit();
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitContext) {
|
|
CARBON_DIAGNOSTIC_CONTEXT(TestContext, "context");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 2, "context")), IsEmpty())));
|
|
Diagnostics::ContextScope scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestContext); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitSoftContext) {
|
|
CARBON_DIAGNOSTIC_SOFT_CONTEXT(TestSoftContext, "soft context");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 2, "soft context")), IsEmpty())));
|
|
Diagnostics::ContextScope soft_scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestSoftContext); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitSoftContextAndContext) {
|
|
CARBON_DIAGNOSTIC_SOFT_CONTEXT(TestSoftContext, "soft context");
|
|
CARBON_DIAGNOSTIC_CONTEXT(TestContext, "context");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 3, "soft context"),
|
|
IsDiagnosticContext(1, 2, "context")),
|
|
IsEmpty())));
|
|
Diagnostics::ContextScope soft_scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(3, TestSoftContext); });
|
|
Diagnostics::ContextScope scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestContext); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitContextAndSoftContext) {
|
|
CARBON_DIAGNOSTIC_CONTEXT(TestContext, "context");
|
|
CARBON_DIAGNOSTIC_SOFT_CONTEXT(TestSoftContext, "soft context");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 3, "context")), IsEmpty())));
|
|
Diagnostics::ContextScope scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(3, TestContext); });
|
|
// This soft context is dropped, since the earlier context supersedes it.
|
|
Diagnostics::ContextScope soft_scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestSoftContext); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitTwoContext) {
|
|
CARBON_DIAGNOSTIC_CONTEXT(TestContext, "context");
|
|
CARBON_DIAGNOSTIC_CONTEXT(TestContext2, "context 2");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 3, "context"),
|
|
IsDiagnosticContext(1, 2, "context 2")),
|
|
IsEmpty())));
|
|
Diagnostics::ContextScope scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(3, TestContext); });
|
|
Diagnostics::ContextScope scope2(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestContext2); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, EmitTwoSoftContext) {
|
|
CARBON_DIAGNOSTIC_SOFT_CONTEXT(TestSoftContext, "soft context");
|
|
CARBON_DIAGNOSTIC_SOFT_CONTEXT(TestSoftContext2, "soft context 2");
|
|
CARBON_DIAGNOSTIC(TestDiagnostic, Warning, "simple warning");
|
|
EXPECT_CALL(
|
|
consumer_,
|
|
HandleDiagnostic(IsDiagnostic(
|
|
Diagnostics::Level::Warning,
|
|
IsDiagnosticMessage(Diagnostics::Kind::TestDiagnostic,
|
|
Diagnostics::Level::Warning, 1, 1,
|
|
"simple warning"),
|
|
ElementsAre(IsDiagnosticContext(1, 3, "soft context")), IsEmpty())));
|
|
Diagnostics::ContextScope scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(3, TestSoftContext); });
|
|
// This soft context is dropped, since the earlier one supersedes it.
|
|
Diagnostics::ContextScope soft_scope(
|
|
&emitter_, [&](auto& builder) { builder.Attach(2, TestSoftContext2); });
|
|
emitter_.Emit(1, TestDiagnostic);
|
|
}
|
|
|
|
TEST_F(EmitterTest, Flush) {
|
|
bool flushed = false;
|
|
auto flush_fn = [&]() { flushed = true; };
|
|
|
|
{
|
|
FakeEmitter emitter(&consumer_);
|
|
emitter.AddFlushFn(flush_fn);
|
|
|
|
// Registering the function does not flush.
|
|
EXPECT_FALSE(flushed);
|
|
|
|
// Explicit calls to `Flush` should flush.
|
|
emitter.Flush();
|
|
EXPECT_TRUE(flushed);
|
|
flushed = false;
|
|
|
|
{
|
|
Diagnostics::AnnotationScope annot(&emitter, [](auto&) {});
|
|
|
|
// Registering an annotation scope should flush.
|
|
EXPECT_TRUE(flushed);
|
|
flushed = false;
|
|
}
|
|
|
|
// Unregistering an annotation scope should flush.
|
|
EXPECT_TRUE(flushed);
|
|
flushed = false;
|
|
}
|
|
|
|
// Destroying the emitter should not flush, as that could call back into the
|
|
// base class emitter after the derived-class emitter has been destroyed.
|
|
EXPECT_FALSE(flushed);
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon::Testing
|