Roslyn C#代码修复:分步移除多节点失效问题咨询
问题场景
我在开发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

