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 293e826
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
return;
}

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

var codeAction = CodeAction.Create(
$"Replace with {replacementMethod}",
Expand Down Expand Up @@ -173,4 +171,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(Compilation compilation, IMethodSymbol methodSymbol)
{
if (SupportsThrowsAsync(compilation))
{
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 @@ -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 @@ -26,42 +26,46 @@ protected CodeFixCodeActionsVerifier(WorkspaceFactory workspaceFactory)

protected override string AnalyzerSettings { get; } = Json.Encode(new object());

protected async Task VerifyCodeActions(string source, params string[] expectedCodeActionTitles)
protected async Task VerifyCodeActions(string source, NSubstituteVersion version, params string[] expectedCodeActionTitles)
{
var codeActions = await RegisterCodeFixes(source);
var codeActions = await RegisterCodeFixes(source, version);

codeActions.Should().NotBeNull();
codeActions.Select(action => action.Title).Should().BeEquivalentTo(expectedCodeActionTitles ?? Array.Empty<string>());
}

private async Task<List<CodeAction>> RegisterCodeFixes(string source)
protected Task VerifyCodeActions(string source, params string[] expectedCodeActionTitles)
{
using (var workspace = new AdhocWorkspace())
{
var actions = new List<CodeAction>();
var project = AddProject(workspace.CurrentSolution, source);
return VerifyCodeActions(source, NSubstituteVersion.Latest, expectedCodeActionTitles);
}

var document = project.Documents.Single();
private async Task<List<CodeAction>> RegisterCodeFixes(string source, NSubstituteVersion version)
{
using var workspace = new AdhocWorkspace();
var actions = new List<CodeAction>();
var project = AddProject(workspace.CurrentSolution, source);
project = UpdateNSubstituteMetadataReference(project, version);

var compilation = await document.Project.GetCompilationAsync();
var compilationDiagnostics = compilation.GetDiagnostics();
var document = project.Documents.Single();

VerifyNoCompilerDiagnosticErrors(compilationDiagnostics);
var compilation = await document.Project.GetCompilationAsync();
var compilationDiagnostics = compilation.GetDiagnostics();

var analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);
VerifyNoCompilerDiagnosticErrors(compilationDiagnostics);

foreach (var context in analyzerDiagnostics.Select(diagnostic => new CodeFixContext(
document,
analyzerDiagnostics[0],
(action, array) => actions.Add(action),
CancellationToken.None)))
{
await CodeFixProvider.RegisterCodeFixesAsync(context);
}
var analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);

return actions;
foreach (var context in analyzerDiagnostics.Select(diagnostic => new CodeFixContext(
document,
analyzerDiagnostics[0],
(action, array) => actions.Add(action),
CancellationToken.None)))
{
await CodeFixProvider.RegisterCodeFixesAsync(context);
}

return actions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,80 +30,60 @@ protected async Task VerifyFix(
int? codeFixIndex = null,
NSubstituteVersion version = NSubstituteVersion.Latest)
{
using (var workspace = new AdhocWorkspace())
{
var project = AddProject(workspace.CurrentSolution, oldSource);

project = UpdateNSubstituteMetadataReference(project, version);
using var workspace = new AdhocWorkspace();
var project = AddProject(workspace.CurrentSolution, oldSource);

var document = project.Documents.Single();
var compilation = await project.GetCompilationAsync();
project = UpdateNSubstituteMetadataReference(project, version);

var compilerDiagnostics = compilation.GetDiagnostics();
var document = project.Documents.Single();
var compilation = await project.GetCompilationAsync();

VerifyNoCompilerDiagnosticErrors(compilerDiagnostics);
var compilerDiagnostics = compilation.GetDiagnostics();

var analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);
VerifyNoCompilerDiagnosticErrors(compilerDiagnostics);

var previousAnalyzerDiagnostics = analyzerDiagnostics;
var attempts = analyzerDiagnostics.Length;
var analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);

for (var i = 0; i < attempts; ++i)
{
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
await CodeFixProvider.RegisterCodeFixesAsync(context);
var previousAnalyzerDiagnostics = analyzerDiagnostics;
var attempts = analyzerDiagnostics.Length;

if (!actions.Any())
{
break;
}
for (var i = 0; i < attempts; ++i)
{
var actions = new List<CodeAction>();
var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None);
await CodeFixProvider.RegisterCodeFixesAsync(context);

document = await document.ApplyCodeAction(actions[codeFixIndex ?? 0]);
compilation = await document.Project.GetCompilationAsync();
if (!actions.Any())
{
break;
}

compilerDiagnostics = compilation.GetDiagnostics();
document = await document.ApplyCodeAction(actions[codeFixIndex ?? 0]);
compilation = await document.Project.GetCompilationAsync();

VerifyNoCompilerDiagnosticErrors(compilerDiagnostics);
compilerDiagnostics = compilation.GetDiagnostics();

analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);
VerifyNoCompilerDiagnosticErrors(compilerDiagnostics);

// check if there are analyzer diagnostics left after the code fix
var newAnalyzerDiagnostics = analyzerDiagnostics.Except(previousAnalyzerDiagnostics).ToList();
if (analyzerDiagnostics.Length == previousAnalyzerDiagnostics.Length && newAnalyzerDiagnostics.Any())
{
Execute.Assertion.Fail(
$"Fix didn't fix analyzer diagnostics: {newAnalyzerDiagnostics.ToDebugString()} New document:{Environment.NewLine}{await document.ToFullString()}");
}
analyzerDiagnostics = await compilation.GetSortedAnalyzerDiagnostics(
DiagnosticAnalyzer,
project.AnalyzerOptions);

previousAnalyzerDiagnostics = analyzerDiagnostics;
// check if there are analyzer diagnostics left after the code fix
var newAnalyzerDiagnostics = analyzerDiagnostics.Except(previousAnalyzerDiagnostics).ToList();
if (analyzerDiagnostics.Length == previousAnalyzerDiagnostics.Length && newAnalyzerDiagnostics.Any())
{
Execute.Assertion.Fail(
$"Fix didn't fix analyzer diagnostics: {newAnalyzerDiagnostics.ToDebugString()} New document:{Environment.NewLine}{await document.ToFullString()}");
}

var actual = await document.ToFullString();

actual.Should().Be(newSource);
}
}

private static Project UpdateNSubstituteMetadataReference(Project project, NSubstituteVersion version)
{
if (version == NSubstituteVersion.Latest)
{
return project;
previousAnalyzerDiagnostics = analyzerDiagnostics;
}

project = project.RemoveMetadataReference(RuntimeMetadataReference.NSubstituteLatestReference);

project = version switch
{
NSubstituteVersion.NSubstitute4_2_2 => project.AddMetadataReference(RuntimeMetadataReference.NSubstitute422Reference),
_ => throw new ArgumentException($"NSubstitute {version} is not supported", nameof(version))
};
var actual = await document.ToFullString();

return project;
actual.Should().Be(newSource);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace NSubstitute.Analyzers.Tests.Shared.CodeFixProviders;

public interface ISyncOverAsyncThrowsCodeFixActionsVerifier
{
Task CreatesCodeAction_WhenOverloadSupported(string method, string expectedCodeActionTitle);
Task CreatesCodeAction_WhenOverloadSupported(string method, NSubstituteVersion version, string expectedCodeActionTitle);

Task DoesNotCreateCodeAction_WhenOverloadNotSupported(string method);
Task DoesNotCreateCodeAction_WhenOverloadNotSupported(string method, NSubstituteVersion version);
}
19 changes: 19 additions & 0 deletions tests/NSubstitute.Analyzers.Tests.Shared/CodeVerifier.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -45,4 +46,22 @@ protected static void VerifyNoCompilerDiagnosticErrors(ImmutableArray<Diagnostic
Execute.Assertion.Fail($"Compilation failed. Errors encountered {compilationErrorDiagnostics.ToDebugString()}");
}
}

protected static Project UpdateNSubstituteMetadataReference(Project project, NSubstituteVersion version)
{
if (version == NSubstituteVersion.Latest)
{
return project;
}

project = project.RemoveMetadataReference(RuntimeMetadataReference.NSubstituteLatestReference);

project = version switch
{
NSubstituteVersion.NSubstitute4_2_2 => project.AddMetadataReference(RuntimeMetadataReference.NSubstitute422Reference),
_ => throw new ArgumentException($"NSubstitute {version} is not supported", nameof(version))
};

return project;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.Diagnostics;
using NSubstitute.Analyzers.Tests.Shared;
using NSubstitute.Analyzers.Tests.Shared.CodeFixProviders;
using NSubstitute.Analyzers.VisualBasic.CodeFixProviders;
using NSubstitute.Analyzers.VisualBasic.DiagnosticAnalyzers;
Expand All @@ -15,9 +16,11 @@ public class SyncOverAsyncThrowsCodeFixActionsTests : VisualBasicCodeFixActionsV
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 = $@"Imports System
Imports System.Threading.Tasks
Expand All @@ -37,13 +40,15 @@ End Sub
End Class
End Namespace";

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 = $@"Imports System
Imports System.Threading.Tasks
Expand All @@ -64,6 +69,6 @@ End Sub
End Class
End Namespace";

await VerifyCodeActions(source);
await VerifyCodeActions(source, version);
}
}

0 comments on commit 293e826

Please sign in to comment.