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 112adc9
Show file tree
Hide file tree
Showing 21 changed files with 1,543 additions and 114 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@
using Microsoft.CodeAnalysis.CodeFixes;
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, SyntaxNode updatedNameSyntax)
{
var expressionSyntax = invocationExpressionSyntax.Expression;
return invocationExpressionSyntax.WithExpression(MemberAccessExpression(
expressionSyntax.Kind(),
((MemberAccessExpressionSyntax)expressionSyntax).Expression,
(SimpleNameSyntax)updatedNameSyntax));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,48 +39,75 @@ 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, SyntaxNode updatedNameSyntax);

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
? await CreateThrowsAsyncInvocationExpression(
currentInvocationExpression,
invocationSymbol,
context)
: await CreateReturnInvocationExpression(
currentInvocationExpression,
invocationOperation,
invocationSymbol,
context);

documentEditor.ReplaceNode(currentInvocationExpression, updatedInvocationExpression);

return documentEditor.GetChangedDocument();
}

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

var documentEditor = await DocumentEditor.CreateAsync(context.Document);
var syntaxGenerator = documentEditor.Generator;

var nameSyntax = invocationSymbol.IsGenericMethod
? syntaxGenerator.GenericName(updatedMethodName, invocationSymbol.TypeArguments)
: syntaxGenerator.IdentifierName(updatedMethodName);

return UpdateMemberExpression(currentInvocationExpression, nameSyntax);
}

private async Task<SyntaxNode> CreateReturnInvocationExpression(
TInvocationExpressionSyntax currentInvocationExpression,
IInvocationOperation invocationOperation,
IMethodSymbol invocationSymbol,
Expand All @@ -93,26 +120,26 @@ private async Task<SyntaxNode> CreateUpdatedInvocationExpression(
CreateFromExceptionInvocationExpression(syntaxGenerator, invocationOperation);

var returnsMethodName =
invocationSymbol.IsThrowsSyncForAnyArgsMethod() ? "ReturnsForAnyArgs" : "Returns";
invocationSymbol.IsThrowsSyncForAnyArgsMethod() ? "Returns" : "ReturnsForAnyArgs";

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 +153,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 +200,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
@@ -1,12 +1,24 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
using NSubstitute.Analyzers.Shared.CodeFixProviders;
using static Microsoft.CodeAnalysis.VisualBasic.SyntaxFactory;

namespace NSubstitute.Analyzers.VisualBasic.CodeFixProviders;

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

protected override SyntaxNode UpdateMemberExpression(InvocationExpressionSyntax invocationExpressionSyntax, SyntaxNode updatedNameSyntax)
{
var expressionSyntax = invocationExpressionSyntax.Expression;
return invocationExpressionSyntax.WithExpression(MemberAccessExpression(
expressionSyntax.Kind(),
((MemberAccessExpressionSyntax)expressionSyntax).Expression,
Token(SyntaxKind.DotToken),
(SimpleNameSyntax)updatedNameSyntax));
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
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 Down Expand Up @@ -40,7 +42,38 @@ public void Test()
}}
}}
}}";
await VerifyCodeActions(source, expectedCodeActionTitle);
await VerifyCodeActions(source, NSubstituteVersion.NSubstitute4_2_2, expectedCodeActionTitle);
}

[Theory]
[InlineData("Throws", "Replace with ThrowsAsync")]
[InlineData("ThrowsForAnyArgs", "Replace with ThrowsAsyncForAnyArgs")]
public async Task CreatesCodeAction_ForModernSyntax(string method, string expectedCodeActionTitle)
{
var source = $@"using System;
using System.Threading.Tasks;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
namespace MyNamespace
{{
public interface IFoo
{{
Task Bar();
}}
public class FooTests
{{
public void Test()
{{
var substitute = NSubstitute.Substitute.For<IFoo>();
substitute.Bar().{method}(new Exception());
substitute.Bar().{method}(callInfo => new Exception());
substitute.Bar().{method}(createException: callInfo => new Exception());
}}
}}
}}";
await VerifyCodeActions(source, Enumerable.Repeat(expectedCodeActionTitle, 3).ToArray());
}

[Theory]
Expand Down Expand Up @@ -70,6 +103,6 @@ public void Test()
}}
}}
}}";
await VerifyCodeActions(source);
await VerifyCodeActions(source, NSubstituteVersion.NSubstitute4_2_2);
}
}
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 112adc9

Please sign in to comment.