使用代码修复提供程序时如何删除 SyntaxToken

How to remove SyntaxToken when using a Code Fix Provider

我正在开发一个用于格式化代码的代码库。在某些情况下,如果我发现参数列表中的关键字 "params" 是多余的,我必须删除语法标记,或者删除一些不需要的大括号 ("{" "}")。

问题是,从语法树中删除语法标记是如此困难和不安。我已经尝试了许多不同的解决方案,您可以在下面找到这些解决方案,但对于我来说,我无法成功。我想要一个适用于你可能遇到的任何语法标记的解决方案,并且我可能需要从语法树中删除它。

在下面的案例中,我构建了一个当前可以正常运行但代码修复提供程序无法正常工作的分析器...(这是一个使用冗余参数关键字的案例)。

//更新 - 我提供了 Analyzer/Code 修复提供程序/NUnit 测试的完整代码

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

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

        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) as ParameterSyntax;
            if (node == null)
                return;

            if (!node.Modifiers.Any(x => x.IsKind(SyntaxKind.ParamsKeyword)))
                return;

            var oldParameterNode = node;
            var paramList = node.Parent as ParameterListSyntax;
            if (paramList == null)
                return;

            //var newRoot = root.ReplaceNode(
            //                            oldParameterNode.Parent as ParameterListSyntax,
            //                            paramList.WithParameters
            //                            (SyntaxFactory.SeparatedList(paramList.Parameters.ToArray()))
            //                            .WithLeadingTrivia(node.GetLeadingTrivia())
            //                            .WithTrailingTrivia(node.GetTrailingTrivia()))
            //                            .WithAdditionalAnnotations(Formatter.Annotation);

            //var paramsKeyword = (node.Modifiers.FirstOrDefault(x => x.IsKind(SyntaxKind.ParamsKeyword)));
            //var indexParams = node.Modifiers.IndexOf(paramsKeyword);
            //var syntaxListWithoutParams = node.Modifiers.RemoveAt(indexParams);
            //node.ReplaceToken(paramsKeyword, syntaxListWithoutParams.AsEnumerable());
            context.RegisterCodeFix(CodeActionFactory.Create(node.Span, diagnostic.Severity, "Remove 'params' modifier", token =>
            {
              var newNode = SyntaxFactory.Parameter(node.AttributeLists,node.Modifiers.Remove(SyntaxFactory.Token(SyntaxKind.ParamsKeyword)),node.Type,node.Identifier,node.Default);
             var newRoot = root.ReplaceNode(node, newNode);
             return Task.FromResult(document.WithSyntaxRoot(newRoot));
            }), diagnostic);
            //context.RegisterCodeFix(CodeActionFactory.Create(node.SKCpan, diagnostic.Severity, , document.WithSyntaxRoot(newRoot)), diagnostic);
        }
    }
}

这是我的情况下的用例。

using System;
using NUnit.Framework;
using RefactoringEssentials.CSharp.Diagnostics;

namespace RefactoringEssentials.Tests.CSharp.Diagnostics
{
    [TestFixture]
    public class RedundantParamsTests : CSharpDiagnosticTestBase
    {
        [Test]
        public void TestBasicCase()
        {
            Analyze<RedundantParamsAnalyzer>(@"class FooBar
{
    public virtual void Foo(string fmt, object[] args)
    {
    }
}

class FooBar2 : FooBar
{
    public override void Foo(string fmt, $params object[] args$)
    {
        System.Console.WriteLine(fmt, args);
    }
}", @"class FooBar
{
    public virtual void Foo(string fmt, object[] args)
    {
    }
}

class FooBar2 : FooBar
{
    public override void Foo(string fmt, object[] args)
    {
        System.Console.WriteLine(fmt, args);
    }
}");
        }

        [Test]
        public void TestValidCase()
        {
            Analyze<RedundantParamsAnalyzer>(@"class FooBar
{
    public virtual void Foo(string fmt, object[] args)
    {
    }
}

class FooBar2 : FooBar
{
    public override void Foo(string fmt, object[] args)
    {
        System.Console.WriteLine(fmt, args);
    }
}");
        }

        [Test]
        public void ValideParamsUsageTests()
        {
            Analyze<RedundantParamsAnalyzer>(@"class FooBar
{
    public virtual void Foo(string fmt, params object[] args)
    {
    }
}

class FooBar2 : FooBar
{
    public override void Foo(string fmt, params object[] args)
    {
        System.Console.WriteLine(fmt, args);
    }
}");
        }

        [Test]
        public void TestDisable()
        {
            Analyze<RedundantParamsAnalyzer>(@"class FooBar
{
    public virtual void Foo(string fmt, object[] args)
    {
    }
}

class FooBar2 : FooBar
{
    // ReSharper disable once RedundantParams
    public override void Foo(string fmt, params object[] args)
    {
        System.Console.WriteLine(fmt, args);
    }
}");
        }
    }
}

对于那些可能对我如何确定参数是多余的感兴趣的人,这是逻辑:

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 RedundantParamsAnalyzer : DiagnosticAnalyzer
    {
        private static readonly DiagnosticDescriptor descriptor = new DiagnosticDescriptor(
            CSharpDiagnosticIDs.RedundantParamsAnalyzerID,
            GettextCatalog.GetString("'params' is ignored on overrides"),
            GettextCatalog.GetString("'params' is always ignored in overrides"),
            DiagnosticAnalyzerCategories.RedundanciesInDeclarations,
            DiagnosticSeverity.Warning,
            isEnabledByDefault: true,
            helpLinkUri: HelpLink.CreateFor(CSharpDiagnosticIDs.RedundantParamsAnalyzerID),
            customTags: DiagnosticCustomTags.Unnecessary
        );

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

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

        //I think it's a better decision to head in this direction instead of MethodDeclaration.
        private static bool TryGetParamsDiagnostic(SyntaxNodeAnalysisContext nodeContext, out Diagnostic diagnostic)
        {
            diagnostic = default(Diagnostic);
            if (nodeContext.IsFromGeneratedCode())
                return false;

            var paramList = nodeContext.Node as ParameterListSyntax;
            var declaration = paramList?.Parent as MethodDeclarationSyntax;

            if (declaration == null)
                return false;

            if (declaration.Modifiers.Count == 0 || !declaration.Modifiers.Any(SyntaxKind.OverrideKeyword))
                return false;

            var lastParam = declaration.ParameterList.Parameters.LastOrDefault();
            SyntaxToken? paramsModifierToken = null;
            if (lastParam == null)
                return false;

            foreach (var x in lastParam.Modifiers)
            {
                if (x.IsKind(SyntaxKind.ParamsKeyword))
                {
                    paramsModifierToken = x;
                    break;
                }
            }

            if (!paramsModifierToken.HasValue ||
                !paramsModifierToken.Value.IsKind(SyntaxKind.ParamsKeyword))
                return false;

            diagnostic = Diagnostic.Create(descriptor, lastParam.GetLocation());
            return true;
        }
}

在这种情况下,您不想删除整个节点,而只想删除 params 修饰符。由于 SyntaxNodes 是不可变的,因此您需要使用 WithModifiers 方法使用适当的修饰符创建一个新节点:

public async override Task RegisterCodeFixesAsync(CodeFixContext context)
{

    var diagnostic = context.Diagnostics.First();
    context.RegisterCodeFix(CodeAction.Create("Remove 'params' modifier", async token =>
    {

        var document = context.Document;
        var root = await document.GetSyntaxRootAsync(token);

        var fullParameterNode = root.FindNode(diagnostic.Location.SourceSpan, false) as ParameterSyntax;

        // Keep all modifiers except the params
        var newModifiers = fullParameterNode.Modifiers.Where(m => !m.IsKind(SyntaxKind.ParamsKeyword));
        var syntaxModifiers = SyntaxTokenList.Create(new SyntaxToken());
        syntaxModifiers.AddRange(newModifiers);

        var updatedParameterNode = fullParameterNode.WithModifiers(syntaxModifiers);

        var newDoc = document.WithSyntaxRoot(root.ReplaceNode(fullParameterNode, updatedParameterNode));
        return newDoc;
    }, "KEY"), diagnostic);
}

至于语法的通用删除选项,我不知道哪个好用。 Node.ReplaceSyntax 是一个相当复杂的方法,我通常发现使用 ReplaceNodeRemoveNode 方法更容易。