Visitors and rewriters

edit

Each language has three base classes, named after it, with a Visit… method per node type:

ClassDoesUse it to
JsonSyntaxWalkerVisits a node and everything below itCollect or count things, check a document
JsonSyntaxVisitor, JsonSyntaxVisitor<TResult>Calls the Visit… method of the type of a node, and does not go into its childrenCompute a result from a node, choosing what to visit yourself
JsonSyntaxRewriterVisits every node and builds a new tree from the nodes you returnChange every node of a kind in one pass

Replace Json with your language: XmlSyntaxWalker, CssSyntaxRewriter, ShellSyntaxVisitor, and so on.

Walk a tree

Derive from the walker and override the methods of the nodes you need. Call the base method to go on into the children:

private sealed class StringCounter : JsonSyntaxWalker
{
    public int Count { get; private set; }

    public override void VisitJsonString(JsonStringSyntax node)
    {
        Count++;
        base.VisitJsonString(node);
    }
}
var counter = new StringCounter();
counter.Visit(JsonSyntaxTree.ParseText("""{ "a": "x", "b": ["y", 1, "z"] }""").GetRoot());

// Member names are tokens of a JsonMemberSyntax, not JsonStringSyntax values
Assert.Equal(3, counter.Count);

By default, a walker visits the nodes only. Pass SyntaxWalkerDepth.Token or SyntaxWalkerDepth.Trivia to its constructor to visit the tokens, or the tokens and their trivia, too, with VisitToken and VisitTrivia.

SyntaxWalker, in Meziantou.Framework.Language, walks the tree of any language. It has no method per node type: it visits nodes, tokens and trivia, which is enough for code shared by several languages:

private sealed class TokenCounter() : SyntaxWalker(SyntaxWalkerDepth.Token)
{
    public int Count { get; private set; }

    protected override void VisitToken(SyntaxToken token) => Count++;
}

When you need a few nodes rather than a full walk, DescendantNodes().OfType<T>() is shorter. See Find a node.

Rewrite a tree

A rewriter returns a node from each Visit… method. Return a new node to replace the one visited, or call the base method to rewrite its children:

private sealed class RenameMembers : JsonSyntaxRewriter
{
    public override SyntaxNode? VisitJsonMember(JsonMemberSyntax node)
    {
        // Rewrite the value first, so the members of nested objects are renamed too
        var member = (JsonMemberSyntax)base.VisitJsonMember(node)!;

        return member.WithName(member.Name.ToUpperInvariant());
    }
}
var tree = JsonSyntaxTree.ParseText("""{ "a": 1, "b": { "c": 2 } }""");

var rewritten = new RenameMembers().Visit(tree.GetRoot());

Assert.Equal("""{ "A": 1, "B": { "C": 2 } }""", rewritten?.ToFullString());

A rewriter keeps the nodes it does not change: when nothing below a node changed, the node is returned as it was, with its text. Visit on the root returns the new root; on a node further down, it rewrites that part only.

The Visit… methods of a rewriter return SyntaxNode?. Cast the result of Visit to the type you expect.

Visit without recursing

A visitor calls the method that matches the type of a node, and that is all: its DefaultVisit does nothing. Use it when you decide which children to visit, or derive from JsonSyntaxVisitor<TResult> to return a value from each method.