Using Roslyn to Analyze and Rewrite C# Syntax Trees

The .NET Compiler Platform, known as Roslyn, exposes powerful APIs that allow developers to parse, inspect, and transform C# source code programmatically. This guide demonstrates how to consume the Roslyn API to parse source text into an immutable syntax tree, inspect node structures using CSharpSyntaxWalker, and safely transform or refactor code using CSharpSyntaxRewriter.

Prerequisites and Installation

To work with Roslyn syntax trees, install the core C# compiler package via NuGet:

dotnet add package Microsoft.CodeAnalysis.CSharp

1. Parsing Code into a Syntax Tree

A syntax tree represents the lexical and syntactic structure of C# source code. Because syntax trees in Roslyn are fully fidelity and immutable, they represent every token, trivia (whitespace and comments), and syntactic construct.

To parse a raw code string into a syntax tree:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

string programText = @"
    namespace SampleNamespace
    {
        class SampleClass
        {
            void SampleMethod()
            {
                int number = 42;
            }
        }
    }";

SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
CompilationUnitSyntax root = tree.GetCompilationUnitRoot();

2. Analyzing the Syntax Tree with CSharpSyntaxWalker

To inspect code without modifying it, implement a custom class that inherits from CSharpSyntaxWalker. The walker uses the visitor pattern to traverse nodes in the tree.

For example, to find all method declarations:

public class MethodAnalyzer : CSharpSyntaxWalker
{
    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        // Extract the method name
        string methodName = node.Identifier.ValueText;
        string returnType = node.ReturnType.ToString();

        System.Console.WriteLine($"Found method '{methodName}' with return type '{returnType}'.");

        // Continue walking child nodes
        base.VisitMethodDeclaration(node);
    }
}

To execute the analyzer against the syntax root:

var analyzer = new MethodAnalyzer();
analyzer.Visit(root);

3. Rewriting the Syntax Tree with CSharpSyntaxRewriter

Because syntax trees are immutable, nodes cannot be updated in place. Instead, you create a new tree by replacing existing nodes. CSharpSyntaxRewriter is a specialized visitor designed to return modified versions of visited nodes.

The following example rewrites all integer literals by incrementing their values:

public class IncrementLiteralRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode? VisitLiteralExpression(LiteralExpressionSyntax node)
    {
        // Check if the literal is a numeric integer
        if (node.IsKind(SyntaxKind.NumericLiteralExpression) && node.Token.Value is int value)
        {
            // Create a new literal token with incremented value
            var newToken = SyntaxFactory.Literal(
                node.Token.LeadingTrivia,
                (value + 1).ToString(),
                value + 1,
                node.Token.TrailingTrivia
            );

            // Return the updated node
            return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, newToken);
        }

        return base.VisitLiteralExpression(node);
    }
}

4. Applying the Rewriter and Generating Code

To apply the transformation, pass the root node to the rewriter and retrieve the new tree:

var rewriter = new IncrementLiteralRewriter();
SyntaxNode newRoot = rewriter.Visit(root);

// Output the updated source code
string modifiedCode = newRoot.ToFullString();
System.Console.WriteLine(modifiedCode);

The resulting modifiedCode string preserves the formatting and whitespace of the original source text while replacing int number = 42; with int number = 43;.