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
CSharpSyntaxRewriter
and 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 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 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 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