Files
carbon-lang/toolchain/diagnostics/renderer_fuzzer.cpp
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

171 lines
6.5 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
// Fuzzes the diagnostic renderer directly, on `Diagnostic` values a compiler
// would never build: degenerate locations, spans past the end of a line,
// overlapping ranges, control bytes and invalid UTF-8 in every text field, and
// every combination of capabilities. The renderer's contract is that none of
// this is ever the reason a compiler dies while reporting a problem, so what is
// checked is that every form of every shape renders without a crash. Whether
// what it draws fits the width is not checked here: an unbreakable token or a
// path legitimately overhangs, and only the line art must fit, which the
// terminal buffer CHECKs on its own.
#include <cstddef>
#include <cstdint>
#include <string>
#include <utility>
#include "common/check.h"
#include "common/terminal/capabilities.h"
#include "common/terminal/output_buffer_ref.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"
#include "testing/fuzzing/libfuzzer.h"
#include "toolchain/diagnostics/diagnostic.h"
#include "toolchain/diagnostics/renderer.h"
namespace Carbon::Diagnostics {
namespace {
// A cursor over the fuzzer input that hands out bounded values, returning zero
// once the bytes run out so that decoding always terminates.
class Bytes {
public:
Bytes(const unsigned char* data, size_t size) : data_(data), size_(size) {}
// Returns the next byte, or 0 when the input is exhausted.
auto Next() -> uint8_t { return at_ < size_ ? data_[at_++] : 0; }
// Returns the next value in `[0, bound)`, or 0 when `bound` is 0.
auto Below(int bound) -> int { return bound > 0 ? Next() % bound : 0; }
// Returns a signed value spanning a range that includes the degenerate
// location values (-1) the renderer must tolerate.
auto Small() -> int32_t { return static_cast<int32_t>(Next()) - 2; }
// Returns a run of the raw input bytes, so text fields carry whatever the
// fuzzer chose -- control bytes, invalid UTF-8, and embedded escapes
// included. The reference is into the input and lives as long as it does.
auto Text() -> llvm::StringRef {
size_t len = Next();
if (at_ + len > size_) {
len = size_ - at_;
}
llvm::StringRef result(reinterpret_cast<const char*>(data_ + at_), len);
at_ += len;
return result;
}
private:
const unsigned char* data_;
size_t size_;
size_t at_ = 0;
};
// Builds a `Loc` from the fuzzer, sharing one buffer for `line`/`file_text` so
// the renderer's pointer-containment paths are exercised rather than always
// failing their guards.
static auto MakeLoc(Bytes& bytes, llvm::StringRef file_text) -> Loc {
llvm::StringRef line = file_text;
// Sometimes point `line` into `file_text` (a real slice), sometimes not.
if (!file_text.empty() && (bytes.Next() & 1)) {
size_t start = bytes.Next() % file_text.size();
line = file_text.substr(start, bytes.Next());
}
return {.filename = bytes.Text(),
.line = line,
.file_text = file_text,
.line_number = bytes.Small(),
.column_number = bytes.Small(),
.length = bytes.Small()};
}
// A format function that ignores the format and returns fuzzer-chosen text, so
// a part's rendered words are whatever the input holds.
static auto SaysText(std::string text) -> FormatFn {
return [text = std::move(text)](llvm::StringLiteral /*format*/,
llvm::ArrayRef<llvm::Any> /*args*/) {
return text;
};
}
} // namespace
// NOLINTNEXTLINE: Match the documented fuzzer entry point declaration style.
extern "C" int LLVMFuzzerTestOneInput(const unsigned char* data, size_t size) {
Bytes bytes(data, size);
Terminal::Capabilities capabilities;
capabilities.color_mode = static_cast<Terminal::ColorMode>(bytes.Below(4));
capabilities.charset = static_cast<Terminal::Charset>(bytes.Below(2));
capabilities.background = static_cast<Terminal::Background>(bytes.Below(2));
capabilities.is_terminal = bytes.Next() & 1;
// Sometimes a width, always positive as the field promises. The range spans
// the compact-form threshold and small values a grid could divide badly.
if (bytes.Next() & 1) {
capabilities.columns = 1 + bytes.Below(200);
}
// The source both the message and the labels point into. One shared buffer
// makes elision between two spans reachable.
llvm::StringRef file_text = bytes.Text();
bool include_kind = bytes.Next() & 1;
auto level = static_cast<Level>(bytes.Below(2));
Message message = {.kind = Kind::TestDiagnostic,
.level = level,
.loc = MakeLoc(bytes, file_text),
.format = "",
.format_fn = SaysText(bytes.Text().str())};
// A context leads the diagnostic in place of the message, so this reaches the
// shapes that only exist when one does.
llvm::SmallVector<Context, 0> contexts;
int context_count = bytes.Below(3);
for (int i = 0; i < context_count; ++i) {
contexts.push_back({.loc = MakeLoc(bytes, file_text),
.name = "TestContext",
.format = "",
.format_fn = SaysText(bytes.Text().str())});
}
llvm::SmallVector<Label, 0> labels;
int label_count = bytes.Below(8);
for (int i = 0; i < label_count; ++i) {
auto category = static_cast<LabelCategory>(bytes.Below(2));
llvm::StringRef words = bytes.Text();
labels.push_back(
{.category = category,
.loc = MakeLoc(bytes, file_text),
.name = "TestLabel",
.format = "",
.format_fn = words.empty() ? FormatFn() : SaysText(words.str())});
}
Diagnostic diagnostic = {.level = level,
.message = std::move(message),
.contexts = std::move(contexts),
.labels = std::move(labels)};
Renderer renderer(capabilities);
renderer.set_include_kind(include_kind);
// The contract is that no shape of diagnostic, however malformed, is ever the
// reason a compiler dies while reporting a problem. Every form has to hold to
// it, so both are drawn; a crash in either is a crash in the error path.
for (bool snippets : {true, false}) {
renderer.set_snippets(snippets);
llvm::SmallString<256> rendered;
Terminal::OutputBufferRef out(rendered);
renderer.Render(out, diagnostic);
}
return 0;
}
} // namespace Carbon::Diagnostics