如何使用来自另一个节点的代码修复提供程序访问和修改节点
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,解决我的问题真的很简单。
我有一个情况需要修改用户写这种代码的情况:
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,解决我的问题真的很简单。