如何从另一个节点访问和修改具有代码修复提供程序的节点

时间:2015-08-17 15:59:42

标签: c# roslyn

我有一种情况需要修改用户编写此类代码的情况:

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

以下代码:

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

目前,我已经建立了一个可行的分析仪。我将把代码放在下面。基本上,分析器查找if语句并验证if的唯一语句是否是return语句。不仅如此,它还验证了方法声明中的下一个语句是return语句。

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

首先,当节点被替换时,我创建了一个新的根,因为它们不能像字符串那样修改,它是关于同样的事情。我尝试删除该节点,但由于某种原因,该指令被忽略。我调试了它,当我访问下一个节点(ReturnStatement)时,它不是null。

我想我的问题基本上是,我如何构建一个代码修复提供程序,它可以修改节点而不需要“链接”它。

以下是分析器的代码

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);
        }

2 个答案:

答案 0 :(得分:1)

我认为问题可能就在这一行:     var block = node.Parent as BlockSyntax;

您正在使用原始树中的node,原始树中也有.Parent已更新newReturn)。

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

因此,您基本上希望使用等效的returnStatementToBeEliminated,而不是node.Parent下的版本。我们用于此的低级工具称为SyntaxAnnotation,它们具有在树编辑之间向前追踪的属性。您可以在进行任何编辑之前向newRoot添加特定注释,然后进行编辑,然后让node.Parent找到包含注释的节点。

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

答案 1 :(得分:0)

对于这个问题,我参考了以下帖子: How do I create a new root by adding and removing nodes retrieved from the old root?

这篇文章展示了这个名为DocumentEditor的类,它允许用户修改他想要的文档,即使它假设是不可变的。之前的问题是,删除节点后,如果我指的是与该节点有连接的东西,那么关系就会消失,我就可以填充了。 基本上,文档注释说这个类是#34;一个编辑器,用于更改文档的语法树。&#34; 在您完成修改该文档后,您需要创建一个新文档并将其作为代码修复提供程序中的任务返回。 为了解决我在代码修复提供程序中遇到的这个问题,我使用了以下代码:

            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);

由于该课程,解决我的问题非常简单。