One drawback of Roslyn’s immutability is that it can sometimes make it tricky to apply multiple changes to a Document or SyntaxTree. Immutability means that every time we apply changes to a syntax tree, we’re given an entirely new syntax tree. By default we can’t compare nodes across trees, so what do we do when we want to make multiple changes to a syntax tree?
Roslyn gives us four options:
- Use the
CSharpSyntaxRewriterand rewrite from the bottom up (See LRN: Part 5) - Use Annotations (See LRN: Part 13)
- Use
TrackNodes() - Use the
DocumentEditor
The DocumentEditor allows us to make multiple changes to a document and get the resulting document after the changes have been applied. Under the covers, the DocumentEditor is a thin layer over the SyntaxEditor.
We’ll use the DocumentEditor to change:
This file contains hidden or 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
| char key = Console.ReadKey(); | |
| if(key == 'A') | |
| { | |
| Console.WriteLine("You pressed A"); | |
| } | |
| else | |
| { | |
| Console.WriteLine("You didn't press A"); | |
| } |
to:
This file contains hidden or 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
| char key = Console.ReadKey(); | |
| if(key == 'A') | |
| { | |
| LogConditionWasTrue(); | |
| Console.WriteLine("You pressed A"); | |
| } | |
| else | |
| { | |
| Console.WriteLine("You didn't press A"); | |
| LogConditionWasFalse(); | |
| } |
We’ll use the DocumentEditor to simultaneously insert an invocation before the first Console.WriteLine() and to insert another after the second.
Unfortunately there’s a ton of boiler plate when creating a Document from scratch. Typically you’ll get a Document from a Workspace so it shouldn’t be this bad:
This file contains hidden or 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
| var mscorlib = MetadataReference.CreateFromAssembly(typeof(object).Assembly); | |
| var workspace = new AdhocWorkspace(); | |
| var projectId = ProjectId.CreateNewId(); | |
| var versionStamp = VersionStamp.Create(); | |
| var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "projName", LanguageNames.CSharp); | |
| var newProject = workspace.AddProject(projectInfo); | |
| var sourceText = SourceText.From(@" | |
| class C | |
| { | |
| void M() | |
| { | |
| char key = Console.ReadKey(); | |
| if (key == 'A') | |
| { | |
| Console.WriteLine(""You pressed A""); | |
| } | |
| else | |
| { | |
| Console.WriteLine(""You didn't press A""); | |
| } | |
| } | |
| }"); | |
| var document = workspace.AddDocument(newProject.Id, "NewFile.cs", sourceText); | |
| var syntaxRoot = await document.GetSyntaxRootAsync(); | |
| var ifStatement = syntaxRoot.DescendantNodes().OfType<IfStatementSyntax>().Single(); | |
| var conditionWasTrueInvocation = | |
| SyntaxFactory.ExpressionStatement( | |
| SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasTrue")) | |
| .WithArgumentList( | |
| SyntaxFactory.ArgumentList() | |
| .WithOpenParenToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.OpenParenToken)) | |
| .WithCloseParenToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.CloseParenToken)))) | |
| .WithSemicolonToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.SemicolonToken)); | |
| var conditionWasFalseInvocation = | |
| SyntaxFactory.ExpressionStatement( | |
| SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("LogConditionWasFalse")) | |
| .WithArgumentList( | |
| SyntaxFactory.ArgumentList() | |
| .WithOpenParenToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.OpenParenToken)) | |
| .WithCloseParenToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.CloseParenToken)))) | |
| .WithSemicolonToken( | |
| SyntaxFactory.Token( | |
| SyntaxKind.SemicolonToken)); | |
| //Finally… create the document editor | |
| var documentEditor = await DocumentEditor.CreateAsync(document); | |
| //Insert LogConditionWasTrue() before the Console.WriteLine() | |
| documentEditor.InsertBefore(ifStatement.Statement.ChildNodes().Single(), conditionWasTrueInvocation); | |
| //Insert LogConditionWasFalse() after the Console.WriteLine() | |
| documentEditor.InsertAfter(ifStatement.Else.Statement.ChildNodes().Single(), conditionWasFalseInvocation); | |
| var newDocument = documentEditor.GetChangedDocument(); |
All the familiar SyntaxNode methods are here. We can Insert, Replace and Remove nodes as we see fit, all based off of nodes in our original syntax tree. Many people find this approach more intuitive than building an entire CSharpSyntaxRewriter.
It can be somewhat difficult to debug things when they go wrong. When writing this post I was mistakenly trying to insert nodes after ifStatement.Else instead of ifStatement.Else.Statement. I was receiving an InvalidOperationException but the message wasn’t very useful and it took me quite some time to figure out what I was doing wrong. The documentation on InsertNodeAfter says:
This node must be of a compatible type to be placed in the same list containing the existing node.
How can we know which types of nodes are compatible with one another? I don’t think there’s a good answer here. We essentially have to learn which nodes are compatible ourselves. As usual the Syntax Visualizer and Roslyn Quoter are the best tools for figuring out what kinds of nodes you should be creating.
It’s worth noting that the DocumentEditor exposes the SemanticModel of your original document. You may need this when editing the original document and making decisions about what you’d like to change.
It’s also worth noting that the underlying SyntaxEditor exposes a SyntaxGenerator that you can use to build syntax nodes without relying on the more verbose SyntaxFactory.
Thanks. Gists won’t be appeared in the RSS of the blog.
Those SyntaxFactory expressions can be simplified by quite a bit. Roslyn Quoter used to show them more complex than necessary, but it has been improved now.
Is there any way to add a Using statement via the DocumentEditor?
I created a UsingDirectiveSyntax, then I fetched the CompilationUnitSyntax from the document and inserted the Using statement using InsertBefore relative to the first member in the CompilationUnitSyntax.
That leads to an error (unable to cast object from UsingDirectiveSyntax to MemberDeclarationSyntax) when I finally call the GetChangedDocument() method.
I don’t know the DocumentEditor well enough to say. I know you can create them with the SyntaxFactory with something like:
SyntaxFactory.CompilationUnit()
.WithUsings(
SyntaxFactory.SingletonList(
SyntaxFactory.UsingDirective(
SyntaxFactory.IdentifierName(“System”))))
Using DocumentEditor can we modify the syntaxtree in parallel threads