Donner uses a unified diagnostics system for all parsers (XML, SVG, CSS, path, transform, etc.). Every diagnostic carries a severity level, a human-readable message, and a precise source range indicating exactly where in the input the problem occurred. A console renderer can display diagnostics with source context and caret/tilde indicators, similar to clang or rustc output.
Issue: https://github.com/jwmcglynn/donner/issues/442
Overview
The diagnostics system has two channels:
- Fatal errors flow through ParseResult<T>, which holds either a successful result, a ParseDiagnostic error, or both (partial result with error).
- Non-fatal warnings flow through ParseWarningSink, a collector passed to parser entry points. When disabled, warning emission is near-zero-cost—the factory callable is never invoked.
flowchart TD
A[Source Text] --> B[XMLParser]
B --> C[SVGParser]
C --> D[AttributeParser]
D --> E["SubParsers: Path, Transform, Color, ..."]
B -->|fatal errors| F["ParseResult<T>"]
C -->|warnings| G[ParseWarningSink]
D -->|warnings| G
E -->|warnings via mergeFromSubparser| G
F --> H[Caller]
G --> H
H --> I[DiagnosticRenderer]
I --> J[Console Output with Carets]
style G fill:#f9f,stroke:#333
style I fill:#bbf,stroke:#333
Headers
Core Types
SourceRange
A half-open interval [start, end) of FileOffset values. Lives in donner/base/FileOffset.h.
struct SourceRange {
FileOffset start;
FileOffset end;
};
Construct ranges directly:
SourceRange{FileOffset::Offset(startPos), FileOffset::Offset(endPos)}
ParseDiagnostic
A diagnostic message with severity, reason string, and source range.
enum class DiagnosticSeverity : uint8_t {
Warning,
Error,
};
DiagnosticSeverity
severity = DiagnosticSeverity::Error;
};
A diagnostic message from a parser, with severity, source range, and human-readable reason.
Definition ParseDiagnostic.h:31
static ParseDiagnostic Warning(RcString reason, FileOffset location)
Create a warning diagnostic at a single offset.
Definition ParseDiagnostic.h:53
static ParseDiagnostic Error(RcString reason, FileOffset location)
Create an error diagnostic at a single offset.
Definition ParseDiagnostic.h:43
RcString reason
Human-readable description of the problem.
Definition ParseDiagnostic.h:36
SourceRange range
Source range that this diagnostic applies to. For point diagnostics where the end is unknown,...
Definition ParseDiagnostic.h:40
DiagnosticSeverity severity
Severity of this diagnostic.
Definition ParseDiagnostic.h:33
The FileOffset overloads create a point range (zero-width) at the given location.
ParseWarningSink
Collects non-fatal warnings during parsing. The key design property is implicit zero-cost suppression: the add(Factory&&) overload accepts a callable that is only invoked when the sink is enabled. This means RcString::fromFormat and other formatting work inside the lambda is automatically skipped when warnings are disabled—callers don't need to check anything.
class ParseWarningSink {
public:
ParseWarningSink() = default;
static ParseWarningSink Disabled();
bool isEnabled() const;
template <typename Factory>
requires std::invocable<Factory> &&
void add(Factory&& factory);
const std::vector<ParseDiagnostic>& warnings() const;
bool hasWarnings() const;
void merge(ParseWarningSink&& other);
void mergeFromSubparser(ParseWarningSink&& other, FileOffset parentOffset);
};
ParseResult<T>
Holds either a successful result, a ParseDiagnostic error, or both (partial result with error). Fatal errors flow through this type; non-fatal warnings flow through ParseWarningSink.
template <typename T>
public:
template <typename Target, typename Functor>
};
A parser result, which may contain a result of type T, or an error, or both.
Definition ParseResult.h:17
bool hasResult() const noexcept
Returns true if this ParseResult contains a valid result.
Definition ParseResult.h:107
bool hasError() const noexcept
Returns true if this ParseResult contains an error.
Definition ParseResult.h:110
T & result() &
Returns the contained result.
Definition ParseResult.h:51
ParseDiagnostic & error() &
Returns the contained error.
Definition ParseResult.h:81
ParseResult< Target > map(const Functor &functor) &&
Map the result of this ParseResult to a new type, by transforming the result with the provided functo...
Definition ParseResult.h:121
ParseResult(T &&result)
Construct from a successful result.
Definition ParseResult.h:22
Usage
Returning errors from a parser
Return a ParseDiagnostic via ParseResult:
SourceRange{FileOffset::Offset(startPos), FileOffset::Offset(endPos)});
Emitting warnings
Use the lazy factory to avoid formatting overhead when warnings are disabled:
warningSink.add([&] {
RcString::fromFormat("Unknown attribute '{}'", std::string_view(name)), range);
});
For literal messages (no formatting), use the direct overload:
Calling a parser
All parser entry points require an explicit ParseWarningSink& parameter. There are no convenience overloads—warning collection is always visible at the call site.
ParseWarningSink warningSink;
auto result = SVGParser::ParseSVG(svgSource, warningSink);
if (result.hasError()) {
}
To discard warnings:
auto disabled = ParseWarningSink::Disabled();
auto result = SVGParser::ParseSVG(svgSource, disabled);
Subparser warning remapping
When a subparser operates on a substring of the parent input, its warnings have local offsets that need remapping to the parent's coordinate space:
ParseWarningSink subSink;
auto subResult = SubParser::Parse(substring, subSink);
parentSink.mergeFromSubparser(std::move(subSink), parentOffset);
Rendering diagnostics
DiagnosticRenderer formats diagnostics with source context, caret/tilde indicators, and optional ANSI colors:
DiagnosticRenderer::Options options;
options.filename = "input.svg";
options.colorize = true;
std::string output = DiagnosticRenderer::format(source, diag, options);
std::string allWarnings = DiagnosticRenderer::formatAll(source, warningSink, options);
Example output:
error: Unexpected character
--> input.svg:1:25
|
1 | <path d="M 100 100 h 2!" />
| ^
For multi-character ranges, tildes indicate the span:
warning: Invalid paint server value
--> 4:12
|
4 | <path fill="url(#)"/>
| ^~~~~~
The renderer handles edge cases gracefully:
- Zero-length (point) ranges: single caret at the insertion point.
- EndOfString offsets: severity label and message only, no source context.
- Out-of-bounds offsets: severity label and message only.
Testing
Test matchers
ParseResultTestUtils.h provides gmock matchers for diagnostics:
EXPECT_THAT(result, ParseErrorIs("Unexpected character"));
EXPECT_THAT(result, ParseErrorRange(Optional(13u), Optional(14u)));
Range-accuracy tests
Each parser has tests verifying that reported ranges point to the correct span:
TEST(PathParser, RangeInvalidFlag) {
auto result = PathParser::Parse("M 0,0 a 1 1 0 2 0 1 1");
ASSERT_THAT(result, AllOf(
ParseErrorIs("Failed to parse arc flag, expected '0' or '1'"),
ParseErrorRange(Optional(13u), Optional(14u))));
}
Renderer golden tests
DiagnosticRenderer_tests.cc uses inline golden strings to catch formatting regressions:
TEST(DiagnosticRenderer, SingleCharError) {
const std::string_view source = R"(<path d="M 100 100 h 2!" />)";
"Unexpected character",
SourceRange{FileOffset::Offset(24), FileOffset::Offset(25)});
DiagnosticRenderer::Options options;
options.filename = "test.svg";
EXPECT_EQ(DiagnosticRenderer::format(source, diag, options),
"error: Unexpected character\n"
" --> test.svg:1:25\n"
" |\n"
" 1 | <path d=\"M 100 100 h 2!\" />\n"
" | ^\n");
}
Architecture Notes
SVGParserContext
SVGParserContext holds a ParseWarningSink& reference for the parse session. It provides addSubparserWarning() and fromSubparser() methods for remapping diagnostics from attribute subparsers (which operate on substrings) back to the original input coordinates.
Performance
- Zero-cost when disabled: ParseWarningSink::Disabled() short-circuits before invoking the factory callable. No formatting, no allocations, no virtual dispatch.
- No additional allocations on success path: ParseResult<T> uses std::optional. ParseDiagnostic is slightly larger than the old ParseError (adds severity + range end), but this only matters on error paths.
- LineOffsets reuse: DiagnosticRenderer::formatAll() computes LineOffsets once and shares it across all diagnostics via a file-local formatWithLineOffsets() helper.
Design decisions
- No convenience overloads: All parser entry points require explicit ParseWarningSink& to make warning collection visible at every call site.
- Concrete class, not virtual: ParseWarningSink is a concrete class with a template add method, not a virtual interface. This enables zero-cost inlining of the enabled check. A virtual interface can be added later if custom sinks are needed.
- Separate overloads instead of default arguments: DiagnosticRenderer uses overloaded methods rather than Options options = {} default arguments because GCC rejects default member initializers in aggregates used as default function arguments before the class is complete.
Not Yet Completed
- Migrate DataUrlParser to use ParseDiagnostic (currently uses std::variant<Result, DataUrlParserError> touching UrlLoader; deferred due to scope).
- Range-accuracy tests for CSS/XML parsers.
Future Work
- Structured error codes for programmatic error handling.
- Fixit suggestions ("did you mean ...?").
- Multi-line range rendering in the diagnostic renderer.
- Source line truncation for very long lines in renderer output.
- LSP-compatible diagnostic output (JSON) for editor integration.
- Machine-readable diagnostic serialization for CI tooling.