Skip to content

Commit

Permalink
Merge pull request #691 from polyadic/argument-order
Browse files Browse the repository at this point in the history
  • Loading branch information
bash authored Nov 1, 2022
2 parents e0ea761 + 010dc0c commit 672e9f1
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 66 deletions.
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

0 comments on commit 672e9f1

Please sign in to comment.