Learn Roslyn Now: Part 13 Keeping track of syntax nodes with Syntax Annotations

It can be tricky to keep track nodes when applying changes to syntax trees. Every time we “change” a tree, we’re really creating a copy of it with our changes applied to that new tree. The moment we do that, any pieces of syntax we had references to earlier become invalid in the context of the new tree.

What’s this mean in practice? It’s tough to keep track of syntax nodes when we change syntax trees.

recent Stack Overflow question touched on this. How can we get the symbol for a class that we’ve just added to a document? We can create a new class declaration, but the moment we add it to the document, we lose track of the node. So how can we keep track of the class so we can get the symbol for it once we’ve added it to the document?

The answer: Use a SyntaxAnnotation

A SyntaxAnnotation is a basically piece of metadata we can attach to a piece of syntax. As we manipulate the tree, the annotation sticks with that piece of syntax making it easy to find.


AdhocWorkspace workspace = new AdhocWorkspace();
Project project = workspace.AddProject("SampleProject", LanguageNames.CSharp);
//Attach a syntax annotation to the class declaration
var syntaxAnnotation = new SyntaxAnnotation();
var classDeclaration = SyntaxFactory.ClassDeclaration("MyClass")
.WithAdditionalAnnotations(syntaxAnnotation);
var compilationUnit = SyntaxFactory.CompilationUnit().AddMembers(classDeclaration);
Document document = project.AddDocument("SampleDocument.cs", compilationUnit);
SemanticModel semanticModel = document.GetSemanticModelAsync().Result;
//Use the annotation on our original node to find the new class declaration
var changedClass = document.GetSyntaxRootAsync().Result.DescendantNodes().OfType<ClassDeclarationSyntax>()
.Where(n => n.HasAnnotation(syntaxAnnotation)).Single();
var symbol = semanticModel.GetDeclaredSymbol(changedClass);

There are a couple of overloads available when creating a SyntaxAnnotation. We can specify Kind and Data to be attached to pieces of syntax. Data is used to attach extra information to a piece of syntax that we’d like to retrieve later. Kind is a field we can use to search for Syntax Annotations.

So instead of looking for the exact instance of our annotation on each node, we could search for annotations based on their kind:


AdhocWorkspace workspace = new AdhocWorkspace();
Project project = workspace.AddProject("Test", LanguageNames.CSharp);
string annotationKind = "SampleKind";
var syntaxAnnotation = new SyntaxAnnotation(annotationKind);
var classDeclaration = SyntaxFactory.ClassDeclaration("MyClass")
.WithAdditionalAnnotations(syntaxAnnotation);
var compilationUnit = SyntaxFactory.CompilationUnit().AddMembers(classDeclaration);
Document document = project.AddDocument("Test.cs", compilationUnit);
SemanticModel semanticModel = await document.GetSemanticModelAsync();
var newAnnotation = new SyntaxAnnotation("test");
//Just search for the Kind instead
var root = await document.GetSyntaxRootAsync();
var changedClass = root.GetAnnotatedNodes(annotationKind).Single();
var symbol = semanticModel.GetDeclaredSymbol(changedClass);

This is just one of a few different ways for dealing with Roslyn’s immutable trees. It’s probably not the easiest to use if you’re making multiple changes and need to track multiple syntax nodes. (If that’s the case, I’d recommend the DocumentEditor). That said, it’s good to be aware of it so you can use it when it makes sense.

6 thoughts on “Learn Roslyn Now: Part 13 Keeping track of syntax nodes with Syntax Annotations

  1. Nice! That’s a handy way to annotate your own nodes, but if you get a syntax tree back in the context of an analyzer, for example, the nodes are already created. Using WithAdditionalAnnotations(), will already introduce a new node, so giving you the “chickin – and – egg”-problem. Or am I missing something?

    1. It depends what you’re trying to do. You can’t annotate nodes and give them back to Visual Studio from an analyzer. (You can’t alter or change nodes at all within an analyzer)

      This stuff would be more useful in the context of a CodeFix. When you’re applying a series of changes to a syntax tree you can use annotations to keep track of nodes as you make your changes.

      1. Sorry, I meant to write code fixer, indeed, but didn’t have enough coffee this morning 😉

        I understand that the approach for tracking nodes in your post is for editing trees, such as for a code fixer. However, I still see the problem that you want to track nodes, but adding the annotation to one of the nodes will give you back a new node + entire new tree, rendering the rest of the nodes you were tracking “disconnected”. And to get back one of the other nodes you wanted to track, from the new tree, it also needs to have an annotation, to find it easily. However, you didn’t added it yet, because you can only add one, at a time.

        Does this clarify my point a little bit better?

      2. >However, I still see the problem that you want to track nodes, but adding the annotation to one of the nodes will give you back a new node + entire new tree, rendering the rest of the nodes you were tracking “disconnected”

        So I think the correct approach would be to apply all your annotations as soon as you can. If you need to mark two nodes then:

        1. Mark the first and get a new tree back.
        2. Mark the second on the new tree and get a new tree back.

        At this point you’ll have a tree that has both annotations applied to it. You can make your changes and trust that your annotations should follow their node around. Only after your annotations have been applied should you start manipulating the tree.

        It’s also possible that managing all this stuff is too complicated to manage with annotations. In this case you might consider the DocumentEditor or the CSharpSyntaxRewriter.

    2. First I want to thank Josh for this great series.

      About Thijs comment.
      If you add annotation you’ve lost the original tree, you right.
      Josh solution to add annotation “as soon as you can” is not so true, because after the first annotating you cant achieve the second node to annotate it. And if you can get it, () you don’t need to annotate it because you already hold it. (unless if you want to edit it more than once).
      I think the right solution in this situation, is to use a DocumentEditor like Josh offered or alternatively use TrackNode or SyntaxRewriter.

      1. Yeah, indeed. It’s a catch 22 situation, or better yet, in “programming terms”, a bootstrapping problem. However, when you’ve migrated the tree you got from the analyzer to an annotated tree, you’re all set to use the annotations the way you want 😉

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s