Skip to content

语法树修改操作

简介

本文档详细介绍如何修改 Roslyn 语法树。你将学习不可变性原则、基本修改操作、以及如何添加、删除和替换节点,同时保留代码格式和注释。

适合人群: 中级到高级开发者
预计阅读时间: 75 分钟
前置知识: 语法树基础、遍历方法


📋 本文导航

章节难度阅读时间链接
不可变性原则🟡 中级10 分钟查看
基本修改操作🟡 中级15 分钟查看
添加节点🟡 中级20 分钟查看
删除节点🟡 中级15 分钟查看
替换节点🟡 中级15 分钟查看
批量修改🔴 高级20 分钟查看
保留格式和注释🔴 高级10 分钟查看

返回: 语法树概览 | 上一篇: 遍历方法 | 下一篇: 性能优化


🟡 不可变性原则

Roslyn 语法树是不可变的,这是理解修改操作的关键。

什么是不可变性?

一旦创建,语法树和节点就不能被修改。所有的"修改"操作实际上都是创建新的树或节点。

为什么要不可变?

优点:

  1. 线程安全: 多个线程可以安全地读取同一个树
  2. 可预测性: 不用担心树在使用过程中被意外修改
  3. 性能优化: 可以安全地共享和缓存节点
  4. 历史追踪: 可以保留修改前后的版本

示例:

csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// ❌ 不能这样做(没有这样的方法)
// classDecl.Identifier = SyntaxFactory.Identifier("Employee");

// ✅ 正确做法:创建新节点
var newClassDecl = classDecl.WithIdentifier(
    SyntaxFactory.Identifier("Employee"));

// 原始节点保持不变
Console.WriteLine(classDecl.Identifier.Text);     // "Person"
Console.WriteLine(newClassDecl.Identifier.Text);  // "Employee"

修改操作的基本模式

核心概念

修改语法树的三步骤:

  1. 找到要修改的节点
  2. 创建修改后的新节点
  3. 用新节点替换原节点,得到新树

🟡 基本修改操作

Roslyn 提供了多种方法来修改节点的不同部分。

With* 方法系列

每个语法节点都有一系列 With* 方法,用于创建修改后的副本。

csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 修改标识符
var newClass1 = classDecl.WithIdentifier(
    SyntaxFactory.Identifier("Employee"));

// 修改修饰符
var newClass2 = classDecl.WithModifiers(
    SyntaxFactory.TokenList(
        SyntaxFactory.Token(SyntaxKind.InternalKeyword)));

// 修改基类列表
var newClass3 = classDecl.WithBaseList(
    SyntaxFactory.BaseList(
        SyntaxFactory.SingletonSeparatedList<BaseTypeSyntax>(
            SyntaxFactory.SimpleBaseType(
                SyntaxFactory.IdentifierName("IDisposable")))));

ReplaceNode 方法

找到节点后,使用 ReplaceNode 在树中替换它。

csharp
var code = @"
public class Person
{
    public string Name { get; set; }
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// 找到类声明
var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 创建新的类声明(修改名称)
var newClassDecl = classDecl.WithIdentifier(
    SyntaxFactory.Identifier("Employee"));

// 在树中替换
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

Console.WriteLine(newRoot.ToFullString());

ReplaceToken 方法

替换单个 token:

csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// 找到 public 关键字
var publicToken = root.DescendantTokens()
    .First(t => t.IsKind(SyntaxKind.PublicKeyword));

// 替换为 internal
var internalToken = SyntaxFactory.Token(SyntaxKind.InternalKeyword)
    .WithTriviaFrom(publicToken); // 保留格式

var newRoot = root.ReplaceToken(publicToken, internalToken);

Console.WriteLine(newRoot.ToFullString());
// 输出: internal class Person { }

注意

使用 ReplaceNodeReplaceToken 后,原始的节点引用将失效。如果需要继续修改,使用新树中的节点。


🟡 添加节点

向类、命名空间等容器节点添加成员。

添加方法到类

csharp
var code = @"
public class Calculator
{
    public int Add(int a, int b) => a + b;
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 创建新方法
var newMethod = SyntaxFactory.MethodDeclaration(
    SyntaxFactory.PredefinedType(
        SyntaxFactory.Token(SyntaxKind.IntKeyword)),
    "Subtract")
    .WithModifiers(
        SyntaxFactory.TokenList(
            SyntaxFactory.Token(SyntaxKind.PublicKeyword)))
    .WithParameterList(
        SyntaxFactory.ParameterList(
            SyntaxFactory.SeparatedList(new[]
            {
                SyntaxFactory.Parameter(
                    SyntaxFactory.Identifier("a"))
                    .WithType(SyntaxFactory.PredefinedType(
                        SyntaxFactory.Token(SyntaxKind.IntKeyword))),
                SyntaxFactory.Parameter(
                    SyntaxFactory.Identifier("b"))
                    .WithType(SyntaxFactory.PredefinedType(
                        SyntaxFactory.Token(SyntaxKind.IntKeyword)))
            })))
    .WithExpressionBody(
        SyntaxFactory.ArrowExpressionClause(
            SyntaxFactory.BinaryExpression(
                SyntaxKind.SubtractExpression,
                SyntaxFactory.IdentifierName("a"),
                SyntaxFactory.IdentifierName("b"))))
    .WithSemicolonToken(
        SyntaxFactory.Token(SyntaxKind.SemicolonToken));

// 添加方法到类
var newClassDecl = classDecl.AddMembers(newMethod);

// 替换类
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

Console.WriteLine(newRoot.ToFullString());

使用 ParseStatement 简化

点击查看简化的添加方法
csharp
// 更简单的方式:解析代码字符串
var methodCode = @"
    public int Subtract(int a, int b) => a - b;
";

var newMethod = SyntaxFactory.ParseMemberDeclaration(methodCode);

var newClassDecl = classDecl.AddMembers(newMethod);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

添加属性

csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 解析属性代码
var property = SyntaxFactory.ParseMemberDeclaration(
    "public string Name { get; set; }");

// 添加属性
var newClassDecl = classDecl.AddMembers(property);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

Console.WriteLine(newRoot.ToFullString());

添加多个成员

csharp
var properties = new[]
{
    "public string Name { get; set; }",
    "public int Age { get; set; }",
    "public string Email { get; set; }"
};

var members = properties
    .Select(p => SyntaxFactory.ParseMemberDeclaration(p))
    .ToArray();

var newClassDecl = classDecl.AddMembers(members);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

提示

使用 ParseMemberDeclarationParseStatement 可以大大简化代码生成,避免手动构建复杂的语法树。


🟡 删除节点

从树中删除节点。

删除方法

csharp
var code = @"
public class Calculator
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;
    public int Multiply(int a, int b) => a * b;
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

// 找到要删除的方法
var methodToRemove = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .First(m => m.Identifier.Text == "Subtract");

// 删除节点
var newRoot = root.RemoveNode(methodToRemove, SyntaxRemoveOptions.KeepNoTrivia);

Console.WriteLine(newRoot.ToFullString());

SyntaxRemoveOptions

删除节点时可以指定如何处理 trivia(空格、注释等):

选项说明
KeepNoTrivia不保留任何 trivia
KeepLeadingTrivia保留前导 trivia
KeepTrailingTrivia保留尾随 trivia
KeepDirectives保留预处理指令
KeepEndOfLine保留换行符
KeepExteriorTrivia保留外部 trivia
KeepUnbalancedDirectives保留不平衡的指令
csharp
// 保留注释
var newRoot1 = root.RemoveNode(methodToRemove, 
    SyntaxRemoveOptions.KeepLeadingTrivia);

// 保留所有格式
var newRoot2 = root.RemoveNode(methodToRemove, 
    SyntaxRemoveOptions.KeepExteriorTrivia | 
    SyntaxRemoveOptions.KeepEndOfLine);

删除多个节点

csharp
// 删除所有私有方法
var privateMethods = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Where(m => m.Modifiers.Any(
        mod => mod.IsKind(SyntaxKind.PrivateKeyword)))
    .ToList();

var newRoot = root.RemoveNodes(privateMethods, 
    SyntaxRemoveOptions.KeepNoTrivia);

条件删除

点击查看条件删除示例
csharp
// 删除所有空方法
var emptyMethods = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Where(m => m.Body != null && 
                m.Body.Statements.Count == 0)
    .ToList();

if (emptyMethods.Any())
{
    var newRoot = root.RemoveNodes(emptyMethods, 
        SyntaxRemoveOptions.KeepNoTrivia);
    Console.WriteLine(newRoot.ToFullString());
}

🟡 替换节点

替换节点是最常用的修改操作。

简单替换

csharp
var code = "public class Person { }";
var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 创建新节点
var newClassDecl = classDecl.WithIdentifier(
    SyntaxFactory.Identifier("Employee"));

// 替换
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

批量替换

csharp
// 将所有 public 方法改为 private
var publicMethods = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Where(m => m.Modifiers.Any(
        mod => mod.IsKind(SyntaxKind.PublicKeyword)))
    .ToList();

var newRoot = root.ReplaceNodes(
    publicMethods,
    (oldNode, newNode) =>
    {
        var publicModifier = newNode.Modifiers
            .First(m => m.IsKind(SyntaxKind.PublicKeyword));
        
        var privateModifier = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
            .WithTriviaFrom(publicModifier);
        
        return newNode.WithModifiers(
            newNode.Modifiers.Replace(publicModifier, privateModifier));
    });

条件替换

点击查看条件替换示例
csharp
// 为所有没有访问修饰符的方法添加 public
var methodsWithoutModifiers = root.DescendantNodes()
    .OfType<MethodDeclarationSyntax>()
    .Where(m => !m.Modifiers.Any())
    .ToList();

var newRoot = root.ReplaceNodes(
    methodsWithoutModifiers,
    (oldNode, newNode) =>
    {
        return newNode.WithModifiers(
            SyntaxFactory.TokenList(
                SyntaxFactory.Token(SyntaxKind.PublicKeyword)));
    });

替换表达式

csharp
// 将所有 a + b 替换为 Add(a, b)
var additions = root.DescendantNodes()
    .OfType<BinaryExpressionSyntax>()
    .Where(b => b.IsKind(SyntaxKind.AddExpression))
    .ToList();

var newRoot = root.ReplaceNodes(
    additions,
    (oldNode, newNode) =>
    {
        return SyntaxFactory.InvocationExpression(
            SyntaxFactory.IdentifierName("Add"),
            SyntaxFactory.ArgumentList(
                SyntaxFactory.SeparatedList(new[]
                {
                    SyntaxFactory.Argument(newNode.Left),
                    SyntaxFactory.Argument(newNode.Right)
                })));
    });

🔴 批量修改

使用 SyntaxRewriter 进行批量修改是最强大的方式。

创建 Rewriter

csharp
public class MethodModifierRewriter : CSharpSyntaxRewriter
{
    private readonly SyntaxKind _fromModifier;
    private readonly SyntaxKind _toModifier;
    
    public MethodModifierRewriter(
        SyntaxKind fromModifier, 
        SyntaxKind toModifier)
    {
        _fromModifier = fromModifier;
        _toModifier = toModifier;
    }
    
    public override SyntaxNode VisitMethodDeclaration(
        MethodDeclarationSyntax node)
    {
        var modifier = node.Modifiers
            .FirstOrDefault(m => m.IsKind(_fromModifier));
        
        if (modifier != default)
        {
            var newModifier = SyntaxFactory.Token(_toModifier)
                .WithTriviaFrom(modifier);
            
            var newModifiers = node.Modifiers.Replace(
                modifier, newModifier);
            
            return node.WithModifiers(newModifiers);
        }
        
        return base.VisitMethodDeclaration(node);
    }
}

// 使用
var rewriter = new MethodModifierRewriter(
    SyntaxKind.PublicKeyword, 
    SyntaxKind.PrivateKeyword);

var newRoot = rewriter.Visit(root);

复杂的 Rewriter 示例

点击查看日志注入 Rewriter
csharp
public class LoggingRewriter : CSharpSyntaxRewriter
{
    public override SyntaxNode VisitMethodDeclaration(
        MethodDeclarationSyntax node)
    {
        // 只处理公共方法
        if (!node.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)))
            return base.VisitMethodDeclaration(node);
        
        // 只处理有方法体的方法
        if (node.Body == null)
            return base.VisitMethodDeclaration(node);
        
        // 创建日志语句
        var logStatement = SyntaxFactory.ParseStatement(
            $"Console.WriteLine(\"[LOG] 进入方法: {node.Identifier.Text}\");\n");
        
        // 在方法体开头添加日志
        var newBody = node.Body.WithStatements(
            node.Body.Statements.Insert(0, logStatement));
        
        var newMethod = node.WithBody(newBody);
        
        return base.VisitMethodDeclaration(newMethod);
    }
}

// 使用
var code = @"
public class UserService
{
    public void CreateUser(string name) 
    {
        // 创建用户逻辑
    }
    
    public void DeleteUser(int id)
    {
        // 删除用户逻辑
    }
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var rewriter = new LoggingRewriter();
var newRoot = rewriter.Visit(root);

Console.WriteLine(newRoot.ToFullString());

链式 Rewriter

csharp
// 可以链式应用多个 rewriter
var root1 = new LoggingRewriter().Visit(root);
var root2 = new MethodModifierRewriter(
    SyntaxKind.PublicKeyword, 
    SyntaxKind.InternalKeyword).Visit(root1);
var root3 = new CommentAdder().Visit(root2);

🔴 保留格式和注释

修改语法树时保留原有的格式和注释很重要。

使用 WithTriviaFrom

csharp
var code = @"
// 这是一个重要的类
public class Person
{
    // 姓名属性
    public string Name { get; set; }
}";

var tree = CSharpSyntaxTree.ParseText(code);
var root = tree.GetRoot();

var classDecl = root.DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .First();

// 修改类名,但保留注释
var newIdentifier = SyntaxFactory.Identifier("Employee")
    .WithTriviaFrom(classDecl.Identifier);

var newClassDecl = classDecl.WithIdentifier(newIdentifier);
var newRoot = root.ReplaceNode(classDecl, newClassDecl);

Console.WriteLine(newRoot.ToFullString());
// 注释被保留

保留前导和尾随 Trivia

csharp
// 保留前导 trivia(注释、空格等)
var newToken = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
    .WithLeadingTrivia(oldToken.LeadingTrivia)
    .WithTrailingTrivia(oldToken.TrailingTrivia);

// 或者使用 WithTriviaFrom 一次性复制
var newToken2 = SyntaxFactory.Token(SyntaxKind.PrivateKeyword)
    .WithTriviaFrom(oldToken);

添加注释

csharp
// 添加单行注释
var comment = SyntaxFactory.Comment("// 这是新添加的注释\n");

var newMethod = method.WithLeadingTrivia(
    method.GetLeadingTrivia().Add(comment));

// 添加文档注释
var docComment = SyntaxFactory.ParseLeadingTrivia(@"
/// <summary>
/// 这是一个新方法
/// </summary>
").ToList();

var newMethod2 = method.WithLeadingTrivia(docComment);

格式化代码

点击查看代码格式化示例
csharp
using Microsoft.CodeAnalysis.Formatting;

// 使用 Formatter 格式化代码
var workspace = new AdhocWorkspace();
var formattedRoot = Formatter.Format(
    newRoot, 
    workspace);

Console.WriteLine(formattedRoot.ToFullString());

最佳实践

  1. 使用 WithTriviaFrom 保留格式
  2. 使用 Formatter.Format 格式化生成的代码
  3. 删除节点时选择合适的 SyntaxRemoveOptions
  4. 添加新节点时考虑添加适当的空格和换行

🔗 相关 API

核心 API 文档

相关文档


📚 下一步

现在你已经掌握了修改语法树的方法,可以继续学习:

或者返回 语法树概览 查看快速参考。


实践建议

  • 始终记住语法树是不可变的
  • 使用 ParseMemberDeclaration 简化代码生成
  • 使用 WithTriviaFrom 保留格式
  • 使用 SyntaxRewriter 进行批量修改
  • 使用 Roslyn Quoter 来生成 SyntaxFactory 代码

基于 MIT 许可发布