Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow arbitrary argument order in analyzers #691

Merged
merged 4 commits into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,52 +18,63 @@ namespace Funcky.Analyzers;
public sealed class EnumerableRepeatNeverCodeFix : CodeFixProvider
{
public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(EnumerableRepeatNeverAnalyzer.DiagnosticId);
=> ImmutableArray.Create(DiagnosticId);

public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

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<InvocationExpressionSyntax>().First() is { } declaration)
if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().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<CancellationToken, Task<Document>> 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<Document> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,57 +18,66 @@ namespace Funcky.Analyzers;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EnumerableRepeatOnceCodeFix))]
public sealed class EnumerableRepeatOnceCodeFix : CodeFixProvider
{
private const string Return = "Return";

public override ImmutableArray<string> FixableDiagnosticIds
=> ImmutableArray.Create(EnumerableRepeatOnceAnalyzer.DiagnosticId);
=> ImmutableArray.Create(DiagnosticId);

public override FixAllProvider GetFixAllProvider()
=> WellKnownFixAllProviders.BatchFixer;

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<InvocationExpressionSyntax>().First() is { } declaration)
if (root?.FindToken(diagnosticSpan.Start).Parent?.AncestorsAndSelf().OfType<InvocationExpressionSyntax>().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<CancellationToken, Task<Document>> 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<Document> 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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public async Task UsingEnumerableRepeatNeverShowsTheSequenceReturnDiagnostic()
await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix<EnumerableRepeatNeverAnalyzer, EnumerableRepeatNeverCodeFix>(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<EnumerableRepeatNeverAnalyzer, EnumerableRepeatNeverCodeFix>(expectedDiagnostic, "RepeatNeverFlipped");
}

[Fact]
public async Task UsingEnumerableRepeatNeverViaConstantShowsTheSequenceReturnDiagnostic()
{
Expand Down
11 changes: 11 additions & 0 deletions Funcky.Analyzers/Funcky.Analyzers.Test/EnumerableRepeatOnceTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnostic()
await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix<EnumerableRepeatOnceAnalyzer, EnumerableRepeatOnceCodeFix>(expectedDiagnostic, "RepeatOnce");
}

[Fact]
public async Task UsingEnumerableRepeatOnceShowsTheSequenceReturnDiagnosticWhenArgumentsAreFlipped()
{
var expectedDiagnostic = VerifyCS
.Diagnostic(EnumerableRepeatOnceAnalyzer.DiagnosticId)
.WithSpan(19, 26, 19, 78)
.WithArguments("\"Hello world!\"");

await VerifyWithSourceExample.VerifyDiagnosticAndCodeFix<EnumerableRepeatOnceAnalyzer, EnumerableRepeatOnceCodeFix>(expectedDiagnostic, "RepeatOnceFlipped");
}

[Fact]
public async Task UsingEnumerableRepeatOnceShowsNoDiagnosticWhenSequenceTypeIsNotAvailable()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Linq;

namespace ConsoleApplication1
{
class Program
{
private void Syntax()
{
var single = Enumerable.Empty<string>();
}
}
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
}
Original file line number Diff line number Diff line change
@@ -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!");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -62,6 +64,7 @@ private static Diagnostic CreateDiagnostic(IInvocationOperation operation, IArgu
=> Diagnostic.Create(
Rule,
operation.Syntax.GetLocation(),
ImmutableDictionary<string, string?>.Empty.Add(ValueParameterIndexProperty, operation.Arguments.IndexOf(valueArgument).ToString()),
valueArgument.Value.Syntax.ToString(),
valueArgument.Value.Type?.ToDisplayString());
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -62,5 +64,6 @@ private static Diagnostic CreateDiagnostic(IInvocationOperation operation, IArgu
=> Diagnostic.Create(
Rule,
operation.Syntax.GetLocation(),
ImmutableDictionary<string, string?>.Empty.Add(ValueParameterIndexProperty, operation.Arguments.IndexOf(valueArgument).ToString()),
valueArgument.Value.Syntax.ToString());
}
8 changes: 4 additions & 4 deletions Funcky.Analyzers/Funcky.Analyzers/OperationMatching.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down