diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixAllProvider.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixAllProvider.cs new file mode 100644 index 00000000..63e4e797 --- /dev/null +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixAllProvider.cs @@ -0,0 +1,26 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeFixes; + +namespace Meziantou.Analyzer.Rules; + +internal sealed class NamedParameterFixAllProvider : DocumentBasedFixAllProvider +{ + public static NamedParameterFixAllProvider Instance { get; } = new(); + + protected override string GetFixAllTitle(FixAllContext fixAllContext) => "Add parameter name"; + + protected override async Task FixAllAsync(FixAllContext fixAllContext, Document document, ImmutableArray diagnostics) + { + if (diagnostics.IsEmpty) + return null; + + foreach (var diagnostic in diagnostics.OrderByDescending(d => d.Location.SourceSpan.Start)) + { + document = await NamedParameterFixer.AddParameterName(document, diagnostic.Location.SourceSpan, fixAllContext.CancellationToken).ConfigureAwait(false); + } + + return document; + } +} diff --git a/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs index a872b0aa..76df21a3 100644 --- a/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs +++ b/src/Meziantou.Analyzer.CodeFixers/Rules/NamedParameterFixer.cs @@ -6,6 +6,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Text; namespace Meziantou.Analyzer.Rules; @@ -14,21 +15,17 @@ public sealed class NamedParameterFixer : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(RuleIdentifiers.UseNamedParameter); - public override FixAllProvider GetFixAllProvider() - { - return WellKnownFixAllProviders.BatchFixer; - } + public override FixAllProvider GetFixAllProvider() => NamedParameterFixAllProvider.Instance; public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - // In case the ArrayCreationExpressionSyntax is wrapped in an ArgumentSyntax or some other node with the same span, - // get the innermost node for ties. - var nodeToFix = root?.FindNode(context.Span, getInnermostNodeForTie: true); - if (nodeToFix is null) + var diagnostic = context.Diagnostics.FirstOrDefault(); + if (root is null || diagnostic is null) return; - var argument = nodeToFix.FirstAncestorOrSelf(); + var argumentSpan = diagnostic.Location.SourceSpan; + var argument = FindArgument(root, argumentSpan); if (argument is null || argument.NameColon is not null) return; @@ -46,18 +43,22 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) var title = "Add parameter name"; var codeAction = CodeAction.Create( title, - ct => AddParameterName(context.Document, nodeToFix, ct), + ct => AddParameterName(context.Document, argumentSpan, ct), equivalenceKey: title); context.RegisterCodeFix(codeAction, context.Diagnostics); } - private static async Task AddParameterName(Document document, SyntaxNode nodeToFix, CancellationToken cancellationToken) + internal static async Task AddParameterName(Document document, TextSpan argumentSpan, CancellationToken cancellationToken) { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + return document; + var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); var semanticModel = editor.SemanticModel; - var argument = nodeToFix.FirstAncestorOrSelf(); + var argument = FindArgument(root, argumentSpan); if (argument is null || argument.NameColon is not null) return document; @@ -75,6 +76,14 @@ private static async Task AddParameterName(Document document, SyntaxNo return editor.GetChangedDocument(); } + private static ArgumentSyntax? FindArgument(SyntaxNode root, TextSpan argumentSpan) + { + // In case the literal is wrapped in an ArgumentSyntax or some other node with the same span, + // get the innermost node for ties. + var nodeToFix = root.FindNode(argumentSpan, getInnermostNodeForTie: true); + return nodeToFix.FirstAncestorOrSelf(); + } + private static ImmutableArray? FindParameters(SemanticModel semanticModel, SyntaxNode? node, CancellationToken cancellationToken) { while (node is not null) diff --git a/tests/Meziantou.Analyzer.Test/Rules/NamedParameterAnalyzerTests.cs b/tests/Meziantou.Analyzer.Test/Rules/NamedParameterAnalyzerTests.cs index b371702c..75f6e50c 100755 --- a/tests/Meziantou.Analyzer.Test/Rules/NamedParameterAnalyzerTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/NamedParameterAnalyzerTests.cs @@ -117,6 +117,41 @@ await CreateProjectBuilder() .ValidateAsync(); } + [Fact] + public async Task BatchFix_MultipleArgumentsInSingleInvocation() + { + const string SourceCode = """ + class TypeName + { + private void InsertStatus(object reviewStatuses, object courseStatuses, object paymentInfos, object utcStatuses, object dmvStatuses) + { + } + + public void Test() + { + this.InsertStatus([|null|], [|null|], [|null|], utcStatuses: null, [|null|]); + } + } + """; + const string CodeFix = """ + class TypeName + { + private void InsertStatus(object reviewStatuses, object courseStatuses, object paymentInfos, object utcStatuses, object dmvStatuses) + { + } + + public void Test() + { + this.InsertStatus(reviewStatuses: null, courseStatuses: null, paymentInfos: null, utcStatuses: null, dmvStatuses: null); + } + } + """; + await CreateProjectBuilder() + .WithSourceCode(SourceCode) + .ShouldBatchFixCodeWith(CodeFix) + .ValidateAsync(); + } + [Fact] public async Task True_WithOptions_ShouldNotReportDiagnostic() {