You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Roslyn C#代码修复:分步移除多节点失效问题咨询

Roslyn代码分析器分步移除节点失效问题解析

问题场景

我在开发C# Roslyn代码分析器与代码修复器时,执行代码变更遇到了节点移除不生效的问题。按照理解的流程,我先获取SyntaxRoot,每次变更生成新Root后继续后续操作,最后返回更新后的文档,但实际运行时只有部分节点被移除。

出问题的代码修复逻辑如下:

private async Task<Document> ApplyCodeFixAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var classDeclaration = methodDeclaration.FirstAncestorOrSelf<ClassDeclarationSyntax>();
    var propertyName = LegacyPropertyAnalyzer.GetPropertyNameFromMethod(methodDeclaration.Identifier.Text);

    if (classDeclaration == null) return document;

    // Find the setter method
    var setterMethodDeclaration = classDeclaration.Members
        .OfType<MethodDeclarationSyntax>()
        .FirstOrDefault(m => m.Identifier.Text.Equals("set" + propertyName, StringComparison.InvariantCultureIgnoreCase));

    // Find the getter method (if needed)
    var getterMethodDeclaration = classDeclaration.Members
        .OfType<MethodDeclarationSyntax>()
        .FirstOrDefault(m => m.Identifier.Text.Equals("get" + propertyName, StringComparison.InvariantCultureIgnoreCase));

    var propertyDeclaration = classDeclaration.Members 
        .OfType<PropertyDeclarationSyntax>()
        .FirstOrDefault(p => p.Identifier.Text.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase));

    var fieldDeclaration = LegacyPropertyAnalyzer.GetFieldFromPropertyMethod(methodDeclaration);

    if (fieldDeclaration != null)
    {
        // Remove the field
        root = root.RemoveNode(fieldDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
    }


    if (setterMethodDeclaration != null)
    {
        // Remove the setter method
        root = root.RemoveNode(setterMethodDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
    }

    if (getterMethodDeclaration != null)
    {
        // Remove the getter method
        root = root.RemoveNode(getterMethodDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
    }

    if (propertyDeclaration != null)
    {
        // Remove the property
        root = root.RemoveNode(propertyDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
    }

    // Return the document with the updated root
    return document.WithSyntaxRoot(root);
}

调试现象

  • 保留字段移除代码时,仅字段被移除,方法和属性未被移除;
  • 注释字段移除代码后,get/set方法被移除,但属性未被移除;
  • 注释方法移除代码后,属性可被移除。

已知一次性调用RemoveNodes可正常工作,想了解分步移除/修改节点的正确方式及该行为的原因。测试代码如下:

class TestMethod2
{   
    private int prop1;

    public int getProp1()
    {
        return prop1;
    }

    public void setProp1(int prop1)
    {
        this.prop1 = prop1;
    }

    public int Prop1 { get => prop1; set => this.prop1 = value; }

    public void foo()
    {
        var x = getProp1();
        setProp1(42);
    }
}

问题原因

Roslyn的语法节点是不可变(Immutable)对象,每次调用RemoveNode都会生成全新的语法树(SyntaxTree)和对应的节点实例。你从原始Root中获取的setterMethodDeclaration、getterMethodDeclaration等节点,属于原始语法树的对象;当第一次调用root.RemoveNode生成新Root后,后续再用这些原始节点调用新Root的RemoveNode时,Roslyn会找不到目标节点——因为新语法树中的节点是全新实例,和原始节点并非同一个对象,自然无法匹配移除。

举个例子:先移除字段生成新Root后,新Root里的类成员集合已经是全新的,你之前拿到的setter方法节点是旧树里的实例,在新树中不存在,所以后续的移除操作会直接失效。

正确的分步移除方式

方案1:每次修改后重新获取目标节点

每次生成新Root后,从新Root中重新查找需要操作的节点,确保使用的是当前语法树的有效节点:

private async Task<Document> ApplyCodeFixAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var classDeclaration = methodDeclaration.FirstAncestorOrSelf<ClassDeclarationSyntax>();
    var propertyName = LegacyPropertyAnalyzer.GetPropertyNameFromMethod(methodDeclaration.Identifier.Text);

    if (classDeclaration == null) return document;

    // 移除字段
    var fieldDeclaration = LegacyPropertyAnalyzer.GetFieldFromPropertyMethod(methodDeclaration);
    if (fieldDeclaration != null)
    {
        root = root.RemoveNode(fieldDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
        // 从新Root重新获取类声明节点
        classDeclaration = root.FindNode(classDeclaration.Span, getInnermostNodeForTie: true) as ClassDeclarationSyntax;
    }

    // 移除setter方法
    if (classDeclaration != null)
    {
        var setterMethodDeclaration = classDeclaration.Members
            .OfType<MethodDeclarationSyntax>()
            .FirstOrDefault(m => m.Identifier.Text.Equals("set" + propertyName, StringComparison.InvariantCultureIgnoreCase));
        if (setterMethodDeclaration != null)
        {
            root = root.RemoveNode(setterMethodDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
            classDeclaration = root.FindNode(classDeclaration.Span, getInnermostNodeForTie: true) as ClassDeclarationSyntax;
        }
    }

    // 移除getter方法
    if (classDeclaration != null)
    {
        var getterMethodDeclaration = classDeclaration.Members
            .OfType<MethodDeclarationSyntax>()
            .FirstOrDefault(m => m.Identifier.Text.Equals("get" + propertyName, StringComparison.InvariantCultureIgnoreCase));
        if (getterMethodDeclaration != null)
        {
            root = root.RemoveNode(getterMethodDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
            classDeclaration = root.FindNode(classDeclaration.Span, getInnermostNodeForTie: true) as ClassDeclarationSyntax;
        }
    }

    // 移除属性
    if (classDeclaration != null)
    {
        var propertyDeclaration = classDeclaration.Members
            .OfType<PropertyDeclarationSyntax>()
            .FirstOrDefault(p => p.Identifier.Text.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase));
        if (propertyDeclaration != null)
        {
            root = root.RemoveNode(propertyDeclaration, SyntaxRemoveOptions.KeepNoTrivia);
        }
    }

    return document.WithSyntaxRoot(root);
}

核心逻辑是每次修改Root后,通过root.FindNode结合原始节点的Span,获取新Root中对应的节点,保证后续操作的是当前语法树的有效实例。

方案2:收集所有节点后批量移除

这种方式更高效,也是已验证可行的方案:先从原始Root中找出所有需要移除的节点,然后一次性调用RemoveNodes批量处理,避免多次修改语法树带来的性能损耗和节点匹配问题:

private async Task<Document> ApplyCodeFixAsync(Document document, MethodDeclarationSyntax methodDeclaration, CancellationToken cancellationToken)
{
    var root = await document.GetSyntaxRootAsync(cancellationToken);
    var classDeclaration = methodDeclaration.FirstAncestorOrSelf<ClassDeclarationSyntax>();
    var propertyName = LegacyPropertyAnalyzer.GetPropertyNameFromMethod(methodDeclaration.Identifier.Text);

    if (classDeclaration == null) return document;

    var nodesToRemove = new List<SyntaxNode>();

    // 收集所有待移除节点
    var fieldDeclaration = LegacyPropertyAnalyzer.GetFieldFromPropertyMethod(methodDeclaration);
    if (fieldDeclaration != null) nodesToRemove.Add(fieldDeclaration);

    var setterMethodDeclaration = classDeclaration.Members
        .OfType<MethodDeclarationSyntax>()
        .FirstOrDefault(m => m.Identifier.Text.Equals("set" + propertyName, StringComparison.InvariantCultureIgnoreCase));
    if (setterMethodDeclaration != null) nodesToRemove.Add(setterMethodDeclaration);

    var getterMethodDeclaration = classDeclaration.Members
        .OfType<MethodDeclarationSyntax>()
        .FirstOrDefault(m => m.Identifier.Text.Equals("get" + propertyName, StringComparison.InvariantCultureIgnoreCase));
    if (getterMethodDeclaration != null) nodesToRemove.Add(getterMethodDeclaration);

    var propertyDeclaration = classDeclaration.Members
        .OfType<PropertyDeclarationSyntax>()
        .FirstOrDefault(p => p.Identifier.Text.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase));
    if (propertyDeclaration != null) nodesToRemove.Add(propertyDeclaration);

    // 批量移除节点
    if (nodesToRemove.Any())
    {
        root = root.RemoveNodes(nodesToRemove, SyntaxRemoveOptions.KeepNoTrivia);
    }

    return document.WithSyntaxRoot(root);
}

内容的提问来源于stack exchange,提问作者user1211286

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 01:37:34