Skip to content

Commit

Permalink
GH-186 - SyncOverAsyncThrowsCodeFixProvider to provide ThrowsAsync fi…
Browse files Browse the repository at this point in the history
…xes for new versions of NSubstitute
  • Loading branch information
tomasz-podolak committed Jul 22, 2022
1 parent 6f5416f commit e49d6c9
Show file tree
Hide file tree
Showing 21 changed files with 980 additions and 122 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NSubstitute.Analyzers.Shared.CodeFixProviders;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace NSubstitute.Analyzers.CSharp.CodeFixProviders;

[ExportCodeFixProvider(LanguageNames.CSharp)]
internal sealed class SyncOverAsyncThrowsCodeFixProvider : AbstractSyncOverAsyncThrowsCodeFixProvider<InvocationExpressionSyntax>
{
protected override SyntaxNode GetExpression(InvocationExpressionSyntax invocationExpressionSyntax) => ((MemberAccessExpressionSyntax)invocationExpressionSyntax.Expression).Expression;

protected override SyntaxNode UpdateMemberExpression(InvocationExpressionSyntax invocationExpressionSyntax, string methodName)
{
var expressionSyntax = invocationExpressionSyntax.Expression;
return invocationExpressionSyntax.WithExpression(MemberAccessExpression(
expressionSyntax.Kind(),
((MemberAccessExpressionSyntax)expressionSyntax).Expression,
IdentifierName(methodName)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,66 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)

var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken);
var methodSymbol = (IMethodSymbol)semanticModel.GetSymbolInfo(invocation).Symbol;
var supportsThrowsAsync = SupportsThrowsAsync(semanticModel.Compilation);

if (methodSymbol.Parameters.Any(param => param.Type.IsCallInfoDelegate(semanticModel)))
if (!supportsThrowsAsync && methodSymbol.Parameters.Any(param => param.Type.IsCallInfoDelegate(semanticModel)))
{
return;
}

var replacementMethod = methodSymbol.IsThrowsSyncForAnyArgsMethod()
? "ReturnsForAnyArgs"
: "Returns";
var replacementMethod = GetReplacementMethodName(methodSymbol, useModernSyntax: supportsThrowsAsync);

var codeAction = CodeAction.Create(
$"Replace with {replacementMethod}",
ct => CreateChangedDocument(context, semanticModel, invocation, methodSymbol, ct),
ct => CreateChangedDocument(context, semanticModel, invocation, methodSymbol, supportsThrowsAsync, ct),
nameof(AbstractSyncOverAsyncThrowsCodeFixProvider<TInvocationExpressionSyntax>));

context.RegisterCodeFix(codeAction, diagnostic);
}

protected abstract SyntaxNode GetExpression(TInvocationExpressionSyntax invocationExpressionSyntax);

protected abstract SyntaxNode UpdateMemberExpression(TInvocationExpressionSyntax invocationExpressionSyntax, string methodName);

private async Task<Document> CreateChangedDocument(
CodeFixContext context,
SemanticModel semanticModel,
TInvocationExpressionSyntax currentInvocationExpression,
IMethodSymbol invocationSymbol,
bool useModernSyntax,
CancellationToken cancellationToken)
{
var documentEditor = await DocumentEditor.CreateAsync(context.Document, cancellationToken);
var invocationOperation = (IInvocationOperation)semanticModel.GetOperation(currentInvocationExpression);

var updatedInvocationExpression = await CreateUpdatedInvocationExpression(
currentInvocationExpression,
invocationOperation,
invocationSymbol,
context);
var updatedInvocationExpression = useModernSyntax
? CreateThrowsAsyncInvocationExpression(
currentInvocationExpression,
invocationSymbol)
: await CreateReturnInvocationExpression(
currentInvocationExpression,
invocationOperation,
invocationSymbol,
context);

documentEditor.ReplaceNode(currentInvocationExpression, updatedInvocationExpression);

return documentEditor.GetChangedDocument();
}

private async Task<SyntaxNode> CreateUpdatedInvocationExpression(
private SyntaxNode CreateThrowsAsyncInvocationExpression(
TInvocationExpressionSyntax currentInvocationExpression,
IMethodSymbol invocationSymbol)
{
var updatedMethodName =
invocationSymbol.IsThrowsSyncForAnyArgsMethod()
? MetadataNames.NSubstituteThrowsAsyncMethod
: MetadataNames.NSubstituteThrowsAsyncForAnyArgsMethod;

return UpdateMemberExpression(currentInvocationExpression, updatedMethodName);
}

private async Task<SyntaxNode> CreateReturnInvocationExpression(
TInvocationExpressionSyntax currentInvocationExpression,
IInvocationOperation invocationOperation,
IMethodSymbol invocationSymbol,
Expand All @@ -97,22 +115,22 @@ private async Task<SyntaxNode> CreateUpdatedInvocationExpression(

if (invocationSymbol.MethodKind == MethodKind.Ordinary)
{
return CreateUpdatedOrdinalInvocationExpression(
return CreateReturnOrdinalInvocationExpression(
currentInvocationExpression,
invocationOperation,
syntaxGenerator,
fromExceptionInvocationExpression,
returnsMethodName);
}

return CreateUpdatedExtensionInvocationExpression(
return CreateReturnExtensionInvocationExpression(
currentInvocationExpression,
syntaxGenerator,
fromExceptionInvocationExpression,
returnsMethodName);
}

private static SyntaxNode CreateUpdatedOrdinalInvocationExpression(
private static SyntaxNode CreateReturnOrdinalInvocationExpression(
TInvocationExpressionSyntax currentInvocationExpression,
IInvocationOperation invocationOperation,
SyntaxGenerator syntaxGenerator,
Expand All @@ -126,7 +144,7 @@ private static SyntaxNode CreateUpdatedOrdinalInvocationExpression(
fromExceptionInvocationExpression).WithTriviaFrom(currentInvocationExpression);
}

private SyntaxNode CreateUpdatedExtensionInvocationExpression(
private SyntaxNode CreateReturnExtensionInvocationExpression(
TInvocationExpressionSyntax currentInvocationExpression,
SyntaxGenerator syntaxGenerator,
SyntaxNode fromExceptionInvocationExpression,
Expand Down Expand Up @@ -173,4 +191,26 @@ private static SyntaxNode GetExceptionCreationExpression(
return invocationOperation.Arguments.OrderBy(arg => arg.Parameter.Ordinal)
.First(arg => arg.Parameter.Ordinal > 0).Value.Syntax;
}

private static bool SupportsThrowsAsync(Compilation compilation)
{
var exceptionExtensionsTypeSymbol = compilation.GetTypeByMetadataName("NSubstitute.ExceptionExtensions.ExceptionExtensions");

return exceptionExtensionsTypeSymbol != null &&
exceptionExtensionsTypeSymbol.GetMembers(MetadataNames.NSubstituteThrowsAsyncMethod).IsEmpty == false;
}

private static string GetReplacementMethodName(IMethodSymbol methodSymbol, bool useModernSyntax)
{
if (useModernSyntax)
{
return methodSymbol.IsThrowsSyncForAnyArgsMethod()
? MetadataNames.NSubstituteThrowsAsyncMethod
: MetadataNames.NSubstituteThrowsAsyncForAnyArgsMethod;
}

return methodSymbol.IsThrowsSyncForAnyArgsMethod()
? MetadataNames.NSubstituteReturnsMethod
: MetadataNames.NSubstituteReturnsForAnyArgsMethod;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public static bool IsThrowsSyncForAnyArgsMethod(this ISymbol symbol)
{
return IsMember(
symbol,
MetadataNames.NSubstituteThrowsForAnyArgsMethod,
MetadataNames.NSubstituteThrowsMethod,
MetadataNames.NSubstituteExceptionExtensionsFullTypeName);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ namespace NSubstitute.Analyzers.VisualBasic.CodeFixProviders;
internal sealed class SyncOverAsyncThrowsCodeFixProvider : AbstractSyncOverAsyncThrowsCodeFixProvider<InvocationExpressionSyntax>
{
protected override SyntaxNode GetExpression(InvocationExpressionSyntax invocationExpressionSyntax) => ((MemberAccessExpressionSyntax)invocationExpressionSyntax.Expression).Expression;

protected override SyntaxNode UpdateMemberExpression(InvocationExpressionSyntax invocationExpressionSyntax, string methodName)
{
return invocationExpressionSyntax;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.CSharp.CodeFixProviders;
using NSubstitute.Analyzers.CSharp.DiagnosticAnalyzers;
using NSubstitute.Analyzers.Tests.Shared;
using NSubstitute.Analyzers.Tests.Shared.CodeFixProviders;
using Xunit;

Expand All @@ -15,9 +16,11 @@ public class SyncOverAsyncThrowsCodeFixActionsTests : CSharpCodeFixActionsVerifi
protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new SyncOverAsyncThrowsAnalyzer();

[Theory]
[InlineData("Throws", "Replace with Returns")]
[InlineData("ThrowsForAnyArgs", "Replace with ReturnsForAnyArgs")]
public async Task CreatesCodeAction_WhenOverloadSupported(string method, string expectedCodeActionTitle)
[InlineData("Throws", NSubstituteVersion.NSubstitute4_2_2, "Replace with Returns")]
[InlineData("ThrowsForAnyArgs", NSubstituteVersion.NSubstitute4_2_2, "Replace with ReturnsForAnyArgs")]
[InlineData("Throws", NSubstituteVersion.Latest, "Replace with ThrowsAsync")]
[InlineData("ThrowsForAnyArgs", NSubstituteVersion.Latest, "Replace with ThrowsAsyncForAnyArgs")]
public async Task CreatesCodeAction_WhenOverloadSupported(string method, NSubstituteVersion version, string expectedCodeActionTitle)
{
var source = $@"using System;
using System.Threading.Tasks;
Expand All @@ -40,13 +43,15 @@ public void Test()
}}
}}
}}";
await VerifyCodeActions(source, expectedCodeActionTitle);
await VerifyCodeActions(source, version, expectedCodeActionTitle);
}

[Theory]
[InlineData("Throws")]
[InlineData("ThrowsForAnyArgs")]
public async Task DoesNotCreateCodeAction_WhenOverloadNotSupported(string method)
[InlineData("Throws", NSubstituteVersion.NSubstitute4_2_2)]
[InlineData("ThrowsForAnyArgs", NSubstituteVersion.NSubstitute4_2_2)]
[InlineData("Throws", NSubstituteVersion.Latest)]
[InlineData("ThrowsForAnyArgs", NSubstituteVersion.Latest)]
public async Task DoesNotCreateCodeAction_WhenOverloadNotSupported(string method, NSubstituteVersion version)
{
var source = $@"using System;
using System.Threading.Tasks;
Expand All @@ -70,6 +75,6 @@ public void Test()
}}
}}
}}";
await VerifyCodeActions(source);
await VerifyCodeActions(source, version);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ public static IEnumerable<object[]> ThrowsTestCases
}
}

public static IEnumerable<object[]> ThrowsAsyncTestCases
{
get
{
yield return new object[] { "Throws", "ThrowsAsync" };
yield return new object[] { "ThrowsForAnyArgs", "ThrowsAsyncForAnyArgs" };
}
}

protected override CodeFixProvider CodeFixProvider { get; } = new SyncOverAsyncThrowsCodeFixProvider();

protected override DiagnosticAnalyzer DiagnosticAnalyzer { get; } = new SyncOverAsyncThrowsAnalyzer();
Expand All @@ -35,4 +44,16 @@ public static IEnumerable<object[]> ThrowsTestCases
[Theory]
[MemberData(nameof(ThrowsTestCases))]
public abstract Task ReplacesThrowsWithReturns_WhenUsedInIndexer(string method, string updatedMethod);

[Theory]
[MemberData(nameof(ThrowsAsyncTestCases))]
public abstract Task ReplacesThrowsWithThrowsAsync_WhenUsedInMethod(string method, string updatedMethod);

[Theory]
[MemberData(nameof(ThrowsAsyncTestCases))]
public abstract Task ReplacesThrowsWithThrowsAsync_WhenUsedInProperty(string method, string updatedMethod);

[Theory]
[MemberData(nameof(ThrowsAsyncTestCases))]
public abstract Task ReplacesThrowsWithThrowsAsync_WhenUsedInIndexer(string method, string updatedMethod);
}
Loading

0 comments on commit e49d6c9

Please sign in to comment.