From 4a6ec61f9b290334875a7c2516f25500d3a387c2 Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister <4602612+bash@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:36:02 +0200 Subject: [PATCH 1/4] Allow arbitrary argument order in repeat never analyzer --- .../EnumerableRepeatNeverCodeFix.cs | 23 ++++++++++--------- .../EnumerableRepeatNeverTest.cs | 11 +++++++++ .../TestCode/RepeatNeverFlipped.expected | 13 +++++++++++ .../TestCode/RepeatNeverFlipped.input | 13 +++++++++++ .../EnumerableRepeatNeverAnalyzer.cs | 3 +++ .../Funcky.Analyzers/OperationMatching.cs | 8 +++---- 6 files changed, 56 insertions(+), 15 deletions(-) create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.expected create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.input diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs index be055766..cd366013 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Simplification; using static Funcky.Analyzers.CodeFixResources; +using static Funcky.Analyzers.EnumerableRepeatNeverAnalyzer; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Funcky.Analyzers; @@ -17,7 +18,7 @@ namespace Funcky.Analyzers; public sealed class EnumerableRepeatNeverCodeFix : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds - => ImmutableArray.Create(EnumerableRepeatNeverAnalyzer.DiagnosticId); + => ImmutableArray.Create(DiagnosticId); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -25,35 +26,35 @@ public override FixAllProvider GetFixAllProvider() public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnosticSpan = context.Diagnostics.First().Location.SourceSpan; + var diagnostic = GetDiagnostic(context); + var diagnosticSpan = diagnostic.Location.SourceSpan; - if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration) + if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration + && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) + && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) { - context.RegisterCodeFix(CreateFix(context, declaration), GetDiagnostic(context)); + context.RegisterCodeFix(CreateFix(context, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration) + private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration, int valueParameterIndex) => CodeAction.Create( EnumerableRepeatNeverCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration), + CreateSequenceReturnAsync(context.Document, declaration, valueParameterIndex), nameof(EnumerableRepeatNeverCodeFixTitle)); - private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration) + private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration, int valueParameterIndex) => async cancellationToken => { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateEnumerableReturnRoot(ExtractFirstArgument(declaration), editor.SemanticModel, editor.Generator)); + editor.ReplaceNode(declaration, CreateEnumerableReturnRoot(declaration.ArgumentList.Arguments[valueParameterIndex], editor.SemanticModel, editor.Generator)); return editor.GetChangedDocument(); }; - private static ArgumentSyntax ExtractFirstArgument(InvocationExpressionSyntax invocationExpr) - => invocationExpr.ArgumentList.Arguments[Argument.First]; - private static SyntaxNode CreateEnumerableReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) => InvocationExpression( MemberAccessExpression( diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatNeverTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatNeverTest.cs index c9178eee..9200c4f3 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatNeverTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatNeverTest.cs @@ -24,6 +24,17 @@ public async Task UsingEnumerableRepeatNeverShowsTheSequenceReturnDiagnostic() await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostic, "RepeatNever"); } + [Fact] + public async Task UsingEnumerableRepeatNeverShowsTheSequenceReturnDiagnosticWhenArgumentsAreFlipped() + { + var expectedDiagnostic = VerifyCS + .Diagnostic(EnumerableRepeatNeverAnalyzer.DiagnosticId) + .WithSpan(10, 26, 10, 78) + .WithArguments("\"Hello world!\"", "string"); + + await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostic, "RepeatNeverFlipped"); + } + [Fact] public async Task UsingEnumerableRepeatNeverViaConstantShowsTheSequenceReturnDiagnostic() { diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.expected new file mode 100644 index 00000000..3197bdde --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.expected @@ -0,0 +1,13 @@ +using System; +using System.Linq; + +namespace ConsoleApplication1 +{ + class Program + { + private void Syntax() + { + var single = Enumerable.Empty(); + } + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.input new file mode 100644 index 00000000..98a6c2d4 --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatNeverFlipped.input @@ -0,0 +1,13 @@ +using System; +using System.Linq; + +namespace ConsoleApplication1 +{ + class Program + { + private void Syntax() + { + var single = Enumerable.Repeat(count: 0, element: "Hello world!"); + } + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatNeverAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatNeverAnalyzer.cs index db29e4a8..1721fff9 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatNeverAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatNeverAnalyzer.cs @@ -12,6 +12,8 @@ namespace Funcky.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class EnumerableRepeatNeverAnalyzer : DiagnosticAnalyzer { + public const string ValueParameterIndexProperty = nameof(ValueParameterIndexProperty); + public const string DiagnosticId = $"{DiagnosticName.Prefix}{DiagnosticName.Usage}02"; private const string Category = nameof(Funcky); @@ -62,6 +64,7 @@ private static Diagnostic CreateDiagnostic(IInvocationOperation operation, IArgu => Diagnostic.Create( Rule, operation.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add(ValueParameterIndexProperty, operation.Arguments.IndexOf(valueArgument).ToString()), valueArgument.Value.Syntax.ToString(), valueArgument.Value.Type?.ToDisplayString()); } diff --git a/Funcky.Analyzers/Funcky.Analyzers/OperationMatching.cs b/Funcky.Analyzers/Funcky.Analyzers/OperationMatching.cs index a4e32219..3b5df291 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/OperationMatching.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/OperationMatching.cs @@ -23,10 +23,10 @@ public static bool MatchArguments( firstArgument = null; secondArgument = null; return operation.Arguments.Length is 2 - && matchFirstArgument(operation.Arguments[0]) - && matchSecondArgument(operation.Arguments[1]) - && (firstArgument = operation.Arguments[0]) is var _ - && (secondArgument = operation.Arguments[1]) is var _; + && (firstArgument = operation.GetArgumentForParameterAtIndex(0)) is var _ + && (secondArgument = operation.GetArgumentForParameterAtIndex(1)) is var _ + && matchFirstArgument(firstArgument) + && matchSecondArgument(secondArgument); } public static bool MatchField( From db4f4471d0527029a3e2033dcf3dfd73ef96b93f Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister <4602612+bash@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:38:13 +0200 Subject: [PATCH 2/4] Allow arbitrary argument order in repeat once analyzer --- .../EnumerableRepeatOnceCodeFix.cs | 25 ++++++++++--------- .../EnumerableRepeatOnceTest.cs | 11 ++++++++ .../TestCode/RepeatOnceFlipped.expected | 22 ++++++++++++++++ .../TestCode/RepeatOnceFlipped.input | 22 ++++++++++++++++ .../EnumerableRepeatOnceAnalyzer.cs | 3 +++ 5 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected create mode 100644 Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs index 5ca63be8..6d12061e 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Simplification; using static Funcky.Analyzers.CodeFixResources; +using static Funcky.Analyzers.EnumerableRepeatOnceAnalyzer; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Funcky.Analyzers; @@ -19,7 +20,7 @@ public sealed class EnumerableRepeatOnceCodeFix : CodeFixProvider private const string Return = "Return"; public override ImmutableArray FixableDiagnosticIds - => ImmutableArray.Create(EnumerableRepeatOnceAnalyzer.DiagnosticId); + => ImmutableArray.Create(DiagnosticId); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -27,38 +28,38 @@ public override FixAllProvider GetFixAllProvider() public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); - var diagnosticSpan = context.Diagnostics.First().Location.SourceSpan; + var diagnostic = GetDiagnostic(context); + var diagnosticSpan = diagnostic.Location.SourceSpan; - if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration) + if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType().First() is { } declaration + && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) + && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) { - context.RegisterCodeFix(CreateFix(context, declaration), GetDiagnostic(context)); + context.RegisterCodeFix(CreateFix(context, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration) + private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration, int valueParameterIndex) => CodeAction.Create( EnumerableRepeatOnceCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration), + CreateSequenceReturnAsync(context.Document, declaration, valueParameterIndex), nameof(EnumerableRepeatOnceCodeFixTitle)); - private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration) + private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration, int valueParameterIndex) => async cancellationToken => { var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateSequenceReturnRoot(ExtractFirstArgument(declaration), editor.SemanticModel, editor.Generator)); + editor.ReplaceNode(declaration, CreateSequenceReturnRoot(declaration.ArgumentList.Arguments[valueParameterIndex], editor.SemanticModel, editor.Generator)); return editor.GetChangedDocument(); }; - private static ArgumentSyntax ExtractFirstArgument(InvocationExpressionSyntax invocationExpression) - => invocationExpression.ArgumentList.Arguments[Argument.First]; - private static SyntaxNode CreateSequenceReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) => SyntaxSequenceReturn(model, generator) - .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument)) + .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument.WithNameColon(null))) .WithCloseParenToken(Token(SyntaxKind.CloseParenToken))) .NormalizeWhitespace(); diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs index eca319b4..ada5b80f 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs @@ -24,6 +24,17 @@ public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnostic() await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostic, "RepeatOnce"); } + [Fact] + public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnosticWhenArgumentsAreFlipped() + { + var expectedDiagnostic = VerifyCS + .Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId) + .WithSpan(19, 26, 19, 78) + .WithArguments("\"Hello world!\""); + + await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix(expectedDiagnostic, "RepeatOnceFlipped"); + } + [Fact] public async Task UsingEnumerableRepeatOnceShowsNoDiagnosticWhenSequenceTypeIsNotAvailable() { diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected new file mode 100644 index 00000000..8f33adfc --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.expected @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Funcky; + +namespace Funcky +{ + class Sequence + { + public static string Return(string value) => value; + } +} + +namespace ConsoleApplication1 +{ + class Program + { + private void Syntax() + { + var single = Sequence.Return("Hello world!"); + } + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input new file mode 100644 index 00000000..d1dabdae --- /dev/null +++ b/Funcky.Analyzers/Funcky.Analyzers.Test/TestCode/RepeatOnceFlipped.input @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Funcky; + +namespace Funcky +{ + class Sequence + { + public static string Return(string value) => value; + } +} + +namespace ConsoleApplication1 +{ + class Program + { + private void Syntax() + { + var single = Enumerable.Repeat(count: 1, element: "Hello world!"); + } + } +} diff --git a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs index 98eac9b1..8371c1eb 100644 --- a/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs +++ b/Funcky.Analyzers/Funcky.Analyzers/EnumerableRepeatOnceAnalyzer.cs @@ -12,6 +12,8 @@ namespace Funcky.Analyzers; [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class EnumerableRepeatOnceAnalyzer : DiagnosticAnalyzer { + public const string ValueParameterIndexProperty = nameof(ValueParameterIndexProperty); + public const string DiagnosticId = $"{DiagnosticName.Prefix}{DiagnosticName.Usage}01"; private const string Category = nameof(Funcky); @@ -62,5 +64,6 @@ private static Diagnostic CreateDiagnostic(IInvocationOperation operation, IArgu => Diagnostic.Create( Rule, operation.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add(ValueParameterIndexProperty, operation.Arguments.IndexOf(valueArgument).ToString()), valueArgument.Value.Syntax.ToString()); } From c89547c34b6c6e39bc846129cf6873c682f64fde Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister <4602612+bash@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:56:42 +0200 Subject: [PATCH 3/4] Implement custom code action --- .../EnumerableRepeatNeverCodeFix.cs | 59 ++++++++++------- .../EnumerableRepeatOnceCodeFix.cs | 63 +++++++++++-------- 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs index cd366013..beba969d 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs @@ -33,37 +33,48 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) { - context.RegisterCodeFix(CreateFix(context, declaration, valueParameterIndex), diagnostic); + context.RegisterCodeFix(new ToEnumerableEmptyCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration, int valueParameterIndex) - => CodeAction.Create( - EnumerableRepeatNeverCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration, valueParameterIndex), - nameof(EnumerableRepeatNeverCodeFixTitle)); + private sealed class ToEnumerableEmptyCodeAction : CodeAction + { + private readonly Document _document; + private readonly InvocationExpressionSyntax _invocationExpression; + private readonly int _valueParameterIndex; - private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration, int valueParameterIndex) - => async cancellationToken - => - { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateEnumerableReturnRoot(declaration.ArgumentList.Arguments[valueParameterIndex], editor.SemanticModel, editor.Generator)); - return editor.GetChangedDocument(); - }; + public ToEnumerableEmptyCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) + { + _document = document; + _invocationExpression = invocationExpression; + _valueParameterIndex = valueParameterIndex; + } - private static SyntaxNode CreateEnumerableReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) - => InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetEnumerableType()!), - GenericName(nameof(Enumerable.Empty)) - .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(CreateTypeFromArgumentType(firstArgument, model))))) - .WithAdditionalAnnotations(Simplifier.Annotation)); + public override string Title => EnumerableRepeatNeverCodeFixTitle; - private static TypeSyntax CreateTypeFromArgumentType(ArgumentSyntax firstArgument, SemanticModel model) - => ParseTypeName(model.GetTypeInfo(firstArgument.Expression).Type?.ToMinimalDisplayString(model, firstArgument.SpanStart) ?? string.Empty); + public override string EquivalenceKey => nameof(ToEnumerableEmptyCodeAction); + + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); + var valueParameter = _invocationExpression.ArgumentList.Arguments[_valueParameterIndex]; + editor.ReplaceNode(_invocationExpression, CreateEnumerableReturnRoot(valueParameter, editor.SemanticModel, editor.Generator)); + return editor.GetChangedDocument(); + } + + private static SyntaxNode CreateEnumerableReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) + => InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetEnumerableType()!), + GenericName(nameof(Enumerable.Empty)) + .WithTypeArgumentList(TypeArgumentList(SingletonSeparatedList(CreateTypeFromArgumentType(firstArgument, model))))) + .WithAdditionalAnnotations(Simplifier.Annotation)); + + private static TypeSyntax CreateTypeFromArgumentType(ArgumentSyntax firstArgument, SemanticModel model) + => ParseTypeName(model.GetTypeInfo(firstArgument.Expression).Type?.ToMinimalDisplayString(model, firstArgument.SpanStart) ?? string.Empty); + } } diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs index 6d12061e..6b7aec3b 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -35,39 +35,50 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) && diagnostic.Properties.TryGetValue(ValueParameterIndexProperty, out var valueParameterIndexProperty) && int.TryParse(valueParameterIndexProperty, out var valueParameterIndex)) { - context.RegisterCodeFix(CreateFix(context, declaration, valueParameterIndex), diagnostic); + context.RegisterCodeFix(new ToSequenceReturnCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration, int valueParameterIndex) - => CodeAction.Create( - EnumerableRepeatOnceCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration, valueParameterIndex), - nameof(EnumerableRepeatOnceCodeFixTitle)); + private sealed class ToSequenceReturnCodeAction : CodeAction + { + private readonly Document _document; + private readonly InvocationExpressionSyntax _invocationExpression; + private readonly int _valueParameterIndex; - private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration, int valueParameterIndex) - => async cancellationToken - => - { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateSequenceReturnRoot(declaration.ArgumentList.Arguments[valueParameterIndex], editor.SemanticModel, editor.Generator)); - return editor.GetChangedDocument(); - }; + public ToSequenceReturnCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) + { + _document = document; + _invocationExpression = invocationExpression; + _valueParameterIndex = valueParameterIndex; + } - private static SyntaxNode CreateSequenceReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) - => SyntaxSequenceReturn(model, generator) - .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument.WithNameColon(null))) - .WithCloseParenToken(Token(SyntaxKind.CloseParenToken))) - .NormalizeWhitespace(); + public override string Title => EnumerableRepeatNeverCodeFixTitle; - private static InvocationExpressionSyntax SyntaxSequenceReturn(SemanticModel model, SyntaxGenerator generator) - => InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetSequenceType()!), - IdentifierName(Return)) - .WithAdditionalAnnotations(Simplifier.Annotation)); + public override string EquivalenceKey => nameof(ToSequenceReturnCodeAction); + + protected override async Task GetChangedDocumentAsync(CancellationToken cancellationToken) + { + var editor = await DocumentEditor.CreateAsync(_document, cancellationToken).ConfigureAwait(false); + var valueArgument = _invocationExpression.ArgumentList.Arguments[_valueParameterIndex]; + editor.ReplaceNode(_invocationExpression, CreateSequenceReturnRoot(valueArgument, editor.SemanticModel, editor.Generator)); + return editor.GetChangedDocument(); + } + + private static SyntaxNode CreateSequenceReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) + => SyntaxSequenceReturn(model, generator) + .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument.WithNameColon(null))) + .WithCloseParenToken(Token(SyntaxKind.CloseParenToken))) + .NormalizeWhitespace(); + + private static InvocationExpressionSyntax SyntaxSequenceReturn(SemanticModel model, SyntaxGenerator generator) + => InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetSequenceType()!), + IdentifierName(Return)) + .WithAdditionalAnnotations(Simplifier.Annotation)); + } } From 010dc0c8444246b1b13138487eafb90fe3c2b332 Mon Sep 17 00:00:00 2001 From: Ruben Schmidmeister <4602612+bash@users.noreply.github.com> Date: Fri, 28 Oct 2022 10:57:04 +0200 Subject: [PATCH 4/4] Use well-known method name --- .../EnumerableRepeatOnceCodeFix.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs index 6b7aec3b..00669f51 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -9,6 +9,7 @@ using Microsoft.CodeAnalysis.Simplification; using static Funcky.Analyzers.CodeFixResources; using static Funcky.Analyzers.EnumerableRepeatOnceAnalyzer; +using static Funcky.Analyzers.FunckyWellKnownMemberNames; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace Funcky.Analyzers; @@ -17,8 +18,6 @@ namespace Funcky.Analyzers; [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EnumerableRepeatOnceCodeFix))] public sealed class EnumerableRepeatOnceCodeFix : CodeFixProvider { - private const string Return = "Return"; - public override ImmutableArray FixableDiagnosticIds => ImmutableArray.Create(DiagnosticId); @@ -78,7 +77,7 @@ private static InvocationExpressionSyntax SyntaxSequenceReturn(SemanticModel mod MemberAccessExpression( SyntaxKind.SimpleMemberAccessExpression, (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetSequenceType()!), - IdentifierName(Return)) + IdentifierName(MonadReturnMethodName)) .WithAdditionalAnnotations(Simplifier.Annotation)); } }