如何使用来自另一个节点的代码修复提供程序访问和修改节点

How to access and modify a node with code fix provider from another node

我有一个情况需要修改用户写这种代码的情况:

bool SomeMethod(object obj)
{
    if(obj == null)
        return false; 
    return true;
}

到以下代码:

bool SomeMethod(object obj)
{
   return obj == null; 
} 

目前,我已经构建了一个可用的分析器。我会把代码放在下面。基本上,分析器查找 if 语句并验证 if 的唯一语句是否是 return statement.Not 仅此而已,它还会验证方法声明中的下一个语句是 return 陈述。

代码修复提供程序查找 ifStatement 的条件并使用该条件创建新的 return 语句。在用 return 语句替换 if 语句后,我想做的是删除第二个 return 语句。在这一点上,我不知道如何失败。

起初,当节点被替换时,我创建了一个新的根,因为它们不能像字符串一样被修改,它是差不多的。我尝试删除该节点,但由于某种原因这条指令被忽略了。而且我已经调试过了,当我访问下一个节点(ReturnStatement)时,它不为空。

我想我的问题基本上是,我如何构建一个代码修复提供程序,它可以在不 "linked" 的情况下修改节点。

这是分析器的代码

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace RefactoringEssentials.CSharp.Diagnostics
{
    [DiagnosticAnalyzer(LanguageNames.CSharp)]
    public class RewriteIfReturnToReturnAnalyzer : DiagnosticAnalyzer
    {
        private static readonly DiagnosticDescriptor descriptor = new DiagnosticDescriptor(
            CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID,
            GettextCatalog.GetString("Convert 'if...return' to 'return'"),
            GettextCatalog.GetString("Convert to 'return' statement"),
            DiagnosticAnalyzerCategories.Opportunities,
            DiagnosticSeverity.Info,
            isEnabledByDefault: true,
            helpLinkUri: HelpLink.CreateFor(CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID)
            );

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(descriptor);

        public override void Initialize(AnalysisContext context)
        {
            context.RegisterSyntaxNodeAction(
                (nodeContext) =>
                {
                    Diagnostic diagnostic;
                    if (TryGetDiagnostic(nodeContext, out diagnostic))
                    {
                        nodeContext.ReportDiagnostic(diagnostic);
                    }
                }, SyntaxKind.IfStatement);
        }

        private static bool TryGetDiagnostic(SyntaxNodeAnalysisContext nodeContext, out Diagnostic diagnostic)
        {
            diagnostic = default(Diagnostic);
            if (nodeContext.IsFromGeneratedCode())
                return false;

            var node = nodeContext.Node as IfStatementSyntax;
            var methodBody = node?.Parent as BlockSyntax;
            var ifStatementIndex = methodBody?.Statements.IndexOf(node);

            if (node?.Statement is ReturnStatementSyntax &&
                methodBody?.Statements.ElementAt(ifStatementIndex.Value + 1) is ReturnStatementSyntax)
            {
                diagnostic = Diagnostic.Create(descriptor, node.GetLocation());
                return true;
            }
            return false;
        }
    }
}

这是代码修复提供程序的代码

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;

namespace RefactoringEssentials.CSharp.Diagnostics
{
    [ExportCodeFixProvider(LanguageNames.CSharp), System.Composition.Shared]
    public class RewriteIfReturnToReturnCodeFixProvider : CodeFixProvider
    {
        public override ImmutableArray<string> FixableDiagnosticIds
        {
            get
            {
                return ImmutableArray.Create(CSharpDiagnosticIDs.RewriteIfReturnToReturnAnalyzerID);
            }
        }

        public override FixAllProvider GetFixAllProvider()
        {
            return WellKnownFixAllProviders.BatchFixer;
        }

        public async override Task RegisterCodeFixesAsync(CodeFixContext context)
        {
            var document = context.Document;
            var cancellationToken = context.CancellationToken;
            var span = context.Span;
            var diagnostics = context.Diagnostics;
            var root = await document.GetSyntaxRootAsync(cancellationToken);
            var diagnostic = diagnostics.First();
            var node = root.FindNode(context.Span);
            if (node == null)
                return;

            context.RegisterCodeFix(
                CodeActionFactory.Create(node.Span, diagnostic.Severity, "Convert to 'return' statement", token =>
                {
                    var statementCondition = (node as IfStatementSyntax)?.Condition;
                    var newReturn = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword),
                        statementCondition, SyntaxFactory.Token(SyntaxKind.SemicolonToken));
                    var newRoot = root.ReplaceNode(node as IfStatementSyntax, newReturn
                        .WithLeadingTrivia(node.GetLeadingTrivia())
                        .WithAdditionalAnnotations(Formatter.Annotation));
                    var block = node.Parent as BlockSyntax;
                    if (block == null)
                        return null;

                    //This code (starting from here) does not do what I'd like to do ...
                    var returnStatementAfterIfStatementIndex = block.Statements.IndexOf(node as IfStatementSyntax) + 1;
                    var returnStatementToBeEliminated = block.Statements.ElementAt(returnStatementAfterIfStatementIndex) as ReturnStatementSyntax;
                    var secondNewRoot = newRoot.RemoveNode(returnStatementToBeEliminated, SyntaxRemoveOptions.KeepNoTrivia);
                    return Task.FromResult(document.WithSyntaxRoot(secondNewRoot));
                }), diagnostic);
        }
    }
}

最后,这是我的 NUnit 测试:

    [Test]
        public void When_Retrurn_Statement_Corrected()
        {
            var input = @"
class TestClass
{
    bool TestMethod (object obj)
    {
        $if (obj != null)
            return true;$
        return false;
    }
}";

            var output = @"
class TestClass
{
    bool TestMethod (object obj)
    {
        return obj!= null;
    }
}";

            Analyze<RewriteIfReturnToReturnAnalyzer>(input, output);
        }

我相信问题可能出在这一行: var block = node.Parent as BlockSyntax;

您正在使用来自 原始 树的 node,以及来自原始树的 .Parent不是 更新后的 newReturn)。

然后,它最终使用这棵不再是最新的旧树计算 returnStatementToBeEliminated,因此当您调用 var secondNewRoot = newRoot.RemoveNode(returnStatementToBeEliminated, SyntaxRemoveOptions.KeepNoTrivia); 时,没有任何反应,因为 newRoot 不包含 returnStatementToBeEliminated.

所以,您基本上想使用 node.Parent 的等效版本,但使用 newRoot 下的版本。我们为此使用的低级工具称为 SyntaxAnnotation,它们具有 属性,它们在树编辑之间向前跟踪。您可以在进行任何编辑之前向 node.Parent 添加特定注释,然后进行编辑,然后让 newRoot 查找带有您的注释的节点。

您可以像这样手动跟踪节点,或者您可以使用 SyntaxEditor class,它将注释部分抽象为更简单的方法,如 TrackNode(还有一些其他不错的SyntaxEditor 中您可能想要查看的功能)。

关于这个问题,我参考了以下post:

这个 post 展示了这个 class 称为 DocumentEditor,它允许用户按照他想要的方式修改文档,即使它被认为是不可变的。之前的问题是,在删除一个节点后,如果我指的是与该节点有连接的东西,关系就会消失,我就可以填充了。 基本上,文档评论说这个 class 是 "an editor for making changes to a document's syntax tree." 修改完该文档后,您需要创建一个新文档并将其 return 作为代码修复提供程序中的任务。 为了解决我的代码修复提供商遇到的这个问题,我使用了以下代码:

            context.RegisterCodeFix(CodeAction.Create("Convert to 'return' statement", async token =>
        {
            var editor = await DocumentEditor.CreateAsync(document, cancellationToken);
            var statementCondition = (node as IfStatementSyntax)?.Condition;
            var newReturn = SyntaxFactory.ReturnStatement(SyntaxFactory.Token(SyntaxKind.ReturnKeyword),
                statementCondition, SyntaxFactory.Token(SyntaxKind.SemicolonToken));
            editor.ReplaceNode(node as IfStatementSyntax, newReturn
                .WithLeadingTrivia(node.GetLeadingTrivia())
                .WithAdditionalAnnotations(Formatter.Annotation));


            var block = node.Parent as BlockSyntax;
            if (block == null)
                return null;

            var returnStatementAfterIfStatementIndex = block.Statements.IndexOf(node as IfStatementSyntax) + 1;
            var returnStatementToBeEliminated = block.Statements.ElementAt(returnStatementAfterIfStatementIndex) as ReturnStatementSyntax;
            editor.RemoveNode(returnStatementToBeEliminated);
            var newDocument = editor.GetChangedDocument();

            return newDocument;
        }, string.Empty), diagnostic);

多亏了 class,解决我的问题真的很简单。