Diagnostics

edit

A parser never throws on invalid text: it reports each problem as a Diagnostic and keeps going. GetDiagnostics() on a tree returns them:

var tree = XmlSyntaxTree.ParseText("<root><child></root>");

var messages = new List<string>();
foreach (var diagnostic in tree.GetDiagnostics())
{
    messages.Add($"{diagnostic.Id} at {diagnostic.Location}: {diagnostic.Message}");
}

Assert.Equal(["XML0001 at (0,6)-(0,13): Missing end tag for 'child'."], messages);

What a diagnostic holds

PropertyHolds
IdA stable identifier, prefixed by the language: JSON0007, XML0001, CSS0011, … Each language page lists its identifiers.
SeverityError or Warning (DiagnosticSeverity also has Info and Hidden)
MessageA sentence for a person, which may change between versions: test the Id in code, not the message
LocationWhere the problem is: a span of the tree's text

ToString() writes the identifier, the position and the message.

Errors and warnings

An error is text the language does not allow. A warning is text the language allows but that is likely a mistake, or that the parser does not know because it may be newer than the parser, such as a duplicate JSON property name or an unknown CSS at-rule:

var tree = JsonSyntaxTree.ParseText("""{ "a": 1, "a": 2 }""");

var diagnostic = Assert.Single(tree.GetDiagnostics());
Assert.Equal("JSON0015", diagnostic.Id);
Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);

// A warning does not make the document invalid
var hasErrors = tree.GetDiagnostics().Any(d => d.Severity == DiagnosticSeverity.Error);
Assert.False(hasErrors);

To check that a document is valid, look for errors, not for an empty list.

Positions

diagnostic.Location is bound to the text of the tree, so it gives lines and columns. They are zero-based:

var tree = XmlSyntaxTree.ParseText("<root>\n  <item>\n</root>");

var diagnostic = Assert.Single(tree.GetDiagnostics());
Assert.Equal("XML0001 at (1,2)-(1,8): Missing end tag for 'item'.", $"{diagnostic.Id} at {diagnostic.Location}: {diagnostic.Message}");

var lineSpan = diagnostic.Location.GetLineSpan();
Assert.Equal(1, lineSpan.Start.Line);      // zero-based
Assert.Equal(2, lineSpan.Start.Character); // zero-based

// The location is bound to the tree's own text
Assert.Same(tree.GetText(), diagnostic.Location.SourceText);

See Source text and positions.

Diagnostics of a node

node.ContainsDiagnostics says whether a node, or anything below it, has a diagnostic, and node.GetDiagnostics() returns them. A diagnostic belongs to the node it is about, and moves with it when you edit the tree.

A few problems depend on the whole document rather than on one node, such as a key defined twice in TOML: the tree works them out when you call GetDiagnostics(). See the page of your language.

Diagnostics after an edit

A tree you create from an edited root, with WithRoot or Create, reports the diagnostics its nodes carry: a node you removed takes its diagnostics with it. CSS and TOML go further and parse the text of the new root again, so they also report a node built by code that does not read back as itself. To check the text of any edited tree, parse root.ToFullString() again.