Changing Syntax Tree uing Roslyn API

01/14/2025
5 minute read

Introduction

In the previous post we have learned how to create a syntax tree, however, sometimes, we want to change the code, and it could be done through changing the syntax tree; since this is a bidirectional relationship. There are two ways to achieve this goal, one is using methods directly available at the instances of types provided by Roslyn, another approach is to use SyntaxRewriter in conjunction with them!

Syntax Tree

Let's consider we have the following code, and we want to change it in a way that if the return type of a method is void it should be changed to a Task, and if it is any other type, it should not be touched!

public class Sample
{
		public void Method(){}
		internal void Method2(){}
		private int Method3(){ return 0;}

		public class Nested
		{
				public void Method(){}
				public int Method2(){ return 0;}
		}
}

Using SharpLab the syntax tree should be something similar to the following image; what we are interested in the syntax tree is the marked nodes, MethodDeclaration, which their representative type in the code is MethodDeclarationSyntax, they have a sub-node as ReturnTypeSyntax.

the node selected in yellow will be replaced by a new one that is returning a task!

Replacement Methods

To achieve that, first we need to get the root node, CompilationUnit, and then select every child node that is of type MethodDeclarationSyntax, since that is the target node in our scenario; The firs argument of the DescendantNodes method, indicates whether it should descend into the children of the selected node or not! The following code, selects all the MethodDeclaration nodes, even those in the Nested class.

var syntaxTree = CSharpSyntaxTree.ParseText(code);
// syntaxTree = SyntaxFactory.ParseSyntaxTree(code);

var root = syntaxTree.GetRoot();

var methods = root
    .DescendantNodes(_ => true)
    .OfType<MethodDeclarationSyntax>();

Now that we have all the tree nodes that we want to replace with a new one, we could use the method ReplaceNodes on the root instance, keep in mind, that this is not an in-place replacement as everything in Roslyn is immutable; that means not only we should use the replace method variations, but also every time we are doing so, we should assign the result to a new variable.

var newRoot = root
    .ReplaceNodes(methods, (original, _) =>
    {
				// Not shown for the berevity of the code, check further below in the post
    });

var newSyntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

Console.WriteLine(newSyntaxTree);

The ReplaceNodes method accepts an IEnumerable<SyntaxNode> and a Func that should return a SyntaxNode replacing the original one! In our scenario, if the original node does not have a return type of void, we will return it without changing anything, if it does, we will change it to Task, the following is the body of the lambda function passed as the second argument to the ReplaceNodes method, in the above-mentioned code:

var returnType = original.ReturnType;

if (returnType.GetFirstToken().Text != SyntaxFactory.Token(SyntaxKind.VoidKeyword).Text)
		return original;

var identifierToken = SyntaxFactory.Identifier("Task")
		.WithTriviaFrom(returnType.GetFirstToken());

var identifierNameSyntaxNode = SyntaxFactory.IdentifierName(identifierToken);

return original.WithReturnType(identifierNameSyntaxNode);

It is worth mentioning that, every syntax token might have some syntax trivia attached to it, as we know, the trivia does not contribute to the semantic of the code, but to the formatting and layout, if we define the identifierToken in the code above, without attaching the trivia from the returnType token, then there would be no space between Task and the method names.

There are various methods available on instances of the objects in the tree that we could leverage to create new syntax nodes, tokens, or trivias, and form a new syntax tree representing completely difference source code. After writing all the above code, you could run the application and see the result, it should be similar to the following:

Syntax Rewriter

Now, lets take a look into another approach using visitor classes, by creating a syntax rewriter that inherits from CSharpSyntaxRewriter. CSharpSyntaxRewriter is a class that extends CSharpSyntaxVisitor<SyntaxNode> and provides default implementations for visiting various C# syntax nodes. By this inheritance there are several VistiXYZ methods to override, XYZ, could be any type we are interested in from the syntax tree, in case of our scenario and MethodDeclaration, it will be VisitMethodDeclaration, and the overridden method should return a SyntaxNode.

Since the logic is the same as the lambda function passed to the ReplaceNodes method, here is the SyntaxRewriter class:

public sealed class MethodDeclarationSyntaxRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitMethodDeclaration(MethodDeclarationSyntax original)
    {
        var returnType = original.ReturnType;

        if (returnType.GetFirstToken().Text != SyntaxFactory.Token(SyntaxKind.VoidKeyword).Text)
            return original;

        var identifierToken = SyntaxFactory.Identifier("Task")
            .WithTriviaFrom(returnType.GetFirstToken());
        
        var identifierNameSyntaxNode = SyntaxFactory.IdentifierName(identifierToken);

        return original.WithReturnType(identifierNameSyntaxNode);
    }
} 

After defining the rewriter class, the usage is as straight forward as creating an instance of it and passing the root object to its Visit method; the result of this code should be identical with the one used in the Replacement Methods section.


var syntaxTree = CSharpSyntaxTree.ParseText(code);
// syntaxTree = SyntaxFactory.ParseSyntaxTree(code);

var root = syntaxTree.GetRoot();

var rewriter = new MethodDeclarationSyntaxRewriter();
var newRoot = rewriter.Visit(root);
var newSyntaxTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

Console.WriteLine(newSyntaxTree);

By overriding the Visit* methods and returning new nodes, you can alter the structure of the code while preserving unmodified parts, and remember to override selectively and only parts you need to analyze.

Conclusion

In scenarios where you need automated code transformations such as code fixes, refactorings, or advanced source generation, you could use either of the above-mentioned approaches, Replacement methods, or syntax rewriter. The latter is a bit more performant, as you could change the node while traversing the tree. however, always bear in mind, that in neither of these approaches, the original tree or nodes are not changed and a new one is created due to immutability nature of the Roslyn APIs. Also, using SyntaxTreeRewriter aligns with Roslyn’s immutable design, making it easy to produce entirely new ASTs. Last but not least, always consider whether you should preserve existing trivia!

Thanks for reading so far, enjoy coding and Dametoon Garm [1] 😊

Resources

Buy Me a Coffee at ko-fi.com

Create Syntax Trees using Roslyn APIs

Sometimes when we want to generate code whether it is source generators or code fixes for a code analyzer, it is required to know how a syntax tree could be created using the Roslyn Compiler API. There are two ways, that we will discuss them in this post.

Test Your Roslyn Code Analyzers

In the previous post we have seen how it is possible to create a diagnostic analyzer using Roslyn APIs. Being a TDD advocate it would be disappointing not to talk about how we could test our code analyzer! So let's take a look into it.

Creating Code with Code - Introduction to Roslyn

Compilers used to be a black box, they were accepting a bunch of source files as input and were producing an artifact as an output! There were no means of interaction with any of the phases inside the compiler pipeline. However, with Roslyn, The .NET Compiler Platform SDK, it is not the case anymore!

An error has occurred. This application may no longer respond until reloaded. Reload x