diff --git a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatNeverCodeFix.cs index be055766..beba969d 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,44 +26,55 @@ 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(new ToEnumerableEmptyCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration) - => CodeAction.Create( - EnumerableRepeatNeverCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration), - nameof(EnumerableRepeatNeverCodeFixTitle)); + private sealed class ToEnumerableEmptyCodeAction : CodeAction + { + private readonly Document _document; + private readonly InvocationExpressionSyntax _invocationExpression; + private readonly int _valueParameterIndex; + + public ToEnumerableEmptyCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) + { + _document = document; + _invocationExpression = invocationExpression; + _valueParameterIndex = valueParameterIndex; + } - private static Func> CreateSequenceReturnAsync(Document document, InvocationExpressionSyntax declaration) - => async cancellationToken - => - { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateEnumerableReturnRoot(ExtractFirstArgument(declaration), editor.SemanticModel, editor.Generator)); - return editor.GetChangedDocument(); - }; + public override string Title => EnumerableRepeatNeverCodeFixTitle; - private static ArgumentSyntax ExtractFirstArgument(InvocationExpressionSyntax invocationExpr) - => invocationExpr.ArgumentList.Arguments[Argument.First]; + public override string EquivalenceKey => nameof(ToEnumerableEmptyCodeAction); - 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)); + 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 TypeSyntax CreateTypeFromArgumentType(ArgumentSyntax firstArgument, SemanticModel model) - => ParseTypeName(model.GetTypeInfo(firstArgument.Expression).Type?.ToMinimalDisplayString(model, firstArgument.SpanStart) ?? string.Empty); + 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 5ca63be8..00669f51 100644 --- a/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs +++ b/Funcky.Analyzers/Funcky.Analyzers.CodeFixes/EnumerableRepeatOnceCodeFix.cs @@ -8,6 +8,8 @@ using Microsoft.CodeAnalysis.Editing; 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; @@ -16,10 +18,8 @@ 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(EnumerableRepeatOnceAnalyzer.DiagnosticId); + => ImmutableArray.Create(DiagnosticId); public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; @@ -27,46 +27,57 @@ 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(new ToSequenceReturnCodeAction(context.Document, declaration, valueParameterIndex), diagnostic); } } private static Diagnostic GetDiagnostic(CodeFixContext context) => context.Diagnostics.First(); - private static CodeAction CreateFix(CodeFixContext context, InvocationExpressionSyntax declaration) - => CodeAction.Create( - EnumerableRepeatOnceCodeFixTitle, - CreateSequenceReturnAsync(context.Document, declaration), - 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) - => async cancellationToken - => - { - var editor = await DocumentEditor.CreateAsync(document, cancellationToken).ConfigureAwait(false); - editor.ReplaceNode(declaration, CreateSequenceReturnRoot(ExtractFirstArgument(declaration), editor.SemanticModel, editor.Generator)); - return editor.GetChangedDocument(); - }; + public ToSequenceReturnCodeAction(Document document, InvocationExpressionSyntax invocationExpression, int valueParameterIndex) + { + _document = document; + _invocationExpression = invocationExpression; + _valueParameterIndex = valueParameterIndex; + } - private static ArgumentSyntax ExtractFirstArgument(InvocationExpressionSyntax invocationExpression) - => invocationExpression.ArgumentList.Arguments[Argument.First]; + public override string Title => EnumerableRepeatNeverCodeFixTitle; - private static SyntaxNode CreateSequenceReturnRoot(ArgumentSyntax firstArgument, SemanticModel model, SyntaxGenerator generator) - => SyntaxSequenceReturn(model, generator) - .WithArgumentList(ArgumentList(SingletonSeparatedList(firstArgument)) - .WithCloseParenToken(Token(SyntaxKind.CloseParenToken))) - .NormalizeWhitespace(); + public override string EquivalenceKey => nameof(ToSequenceReturnCodeAction); - private static InvocationExpressionSyntax SyntaxSequenceReturn(SemanticModel model, SyntaxGenerator generator) - => InvocationExpression( - MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - (ExpressionSyntax)generator.TypeExpressionForStaticMemberAccess(model.Compilation.GetSequenceType()!), - IdentifierName(Return)) - .WithAdditionalAnnotations(Simplifier.Annotation)); + 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(MonadReturnMethodName)) + .WithAdditionalAnnotations(Simplifier.Annotation)); + } } 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/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/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.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/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/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()); } 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(