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.
A 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.