Skip to content

Commit

Permalink
Add analyzers to suggest (v2)/require (v3) not using 'async void' in …
Browse files Browse the repository at this point in the history
…test method signature
  • Loading branch information
bradwilson committed Apr 14, 2024
1 parent 0d61dbf commit 2397d14
Show file tree
Hide file tree
Showing 6 changed files with 368 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using System.Composition;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;

namespace Xunit.Analyzers.Fixes;

[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
public class DoNotUseAsyncVoidForTestMethodsFixer : BatchedMemberFixProvider
{
public const string Key_ConvertToTask = "xUnit1048_xUnit1049_ConvertToTask";
public const string Key_ConvertToValueTask = "xUnit1049_ConvertToValueTask";

public DoNotUseAsyncVoidForTestMethodsFixer() :
base(
Descriptors.X1048_DoNotUseAsyncVoidForTestMethods_V2.Id,
Descriptors.X1049_DoNotUseAsyncVoidForTestMethods_V3.Id
)
{ }

public override async Task RegisterCodeFixesAsync(
CodeFixContext context,
ISymbol member)
{
var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false);
if (semanticModel is null)
return;

var taskReturnType = TypeSymbolFactory.Task(semanticModel.Compilation);
if (taskReturnType is null)
return;

var valueTaskReturnType = TypeSymbolFactory.ValueTask(semanticModel.Compilation);

foreach (var diagnostic in context.Diagnostics)
{
context.RegisterCodeFix(
CodeAction.Create(
"Change return type to Task",
ct => context.Document.Project.Solution.ChangeMemberType(member, taskReturnType, ct),
Key_ConvertToTask
),
diagnostic
);

if (valueTaskReturnType is not null && diagnostic.Id == Descriptors.X1049_DoNotUseAsyncVoidForTestMethods_V3.Id)
context.RegisterCodeFix(
CodeAction.Create(
"Change return type to ValueTask",
ct => context.Document.Project.Solution.ChangeMemberType(member, valueTaskReturnType, ct),
Key_ConvertToValueTask
),
diagnostic
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System.Threading.Tasks;
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.DoNotUseAsyncVoidForTestMethods>;

public class DoNotUseAsyncVoidForTestMethodsTests
{
[Fact]
public async Task NonTestMethod_DoesNotTrigger()
{
var source = @"
using System.Threading.Tasks;
public class MyClass {
public async void MyMethod() {
await Task.Yield();
}
}";

await Verify.VerifyAnalyzer(source);
}

[Fact]
public async Task AsyncTaskMethod_DoesNotTrigger()
{
var source = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
await Task.Yield();
}
}";

await Verify.VerifyAnalyzer(source);
}

[Fact]
public async Task AsyncValueTaskMethod_V3_DoesNotTrigger()
{
var source = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async ValueTask TestMethod() {
await Task.Yield();
}
}";

await Verify.VerifyAnalyzerV3(source);
}


[Fact]
public async Task AsyncVoidMethod_Triggers()
{
var source = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async void TestMethod() {
await Task.Yield();
}
}";

var expectedV2 =
Verify
.Diagnostic("xUnit1048")
.WithSpan(7, 23, 7, 33);
var expectedV3 =
Verify
.Diagnostic("xUnit1049")
.WithSpan(7, 23, 7, 33);

await Verify.VerifyAnalyzerV2(source, expectedV2);
await Verify.VerifyAnalyzerV3(source, expectedV3);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
using System.Threading.Tasks;
using Xunit.Analyzers.Fixes;
using Verify = CSharpVerifier<Xunit.Analyzers.DoNotUseAsyncVoidForTestMethods>;

namespace Xunit.Analyzers;

public class DoNotUseAsyncVoidForTestMethodsFixerTests
{
[Fact]
public async Task WithoutNamespace_ConvertsToTask()
{
var beforeV2 = @"
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1048:TestMethod|}() {
await System.Threading.Tasks.Task.Yield();
}
}";
var beforeV3 = @"
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1049:TestMethod|}() {
await System.Threading.Tasks.Task.Yield();
}
}";
var after = @"
using Xunit;
public class TestClass {
[Fact]
public async System.Threading.Tasks.Task TestMethod() {
await System.Threading.Tasks.Task.Yield();
}
}";

await Verify.VerifyCodeFixV2(beforeV2, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToTask);
await Verify.VerifyCodeFixV3(beforeV3, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToTask);
}

[Fact]
public async Task WithNamespace_ConvertsToTask()
{
var beforeV2 = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1048:TestMethod|}() {
await Task.Yield();
}
}";
var beforeV3 = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1049:TestMethod|}() {
await Task.Yield();
}
}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async Task TestMethod() {
await Task.Yield();
}
}";

await Verify.VerifyCodeFixV2(beforeV2, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToTask);
await Verify.VerifyCodeFixV3(beforeV3, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToTask);
}

[Fact]
public async Task WithoutNamespace_ConvertsToValueTask()
{
var before = @"
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1049:TestMethod|}() {
await System.Threading.Tasks.Task.Yield();
}
}";
var after = @"
using Xunit;
public class TestClass {
[Fact]
public async System.Threading.Tasks.ValueTask TestMethod() {
await System.Threading.Tasks.Task.Yield();
}
}";

await Verify.VerifyCodeFixV3(before, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToValueTask);
}

[Fact]
public async Task WithNamespace_ConvertsToValueTask()
{
var before = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async void {|xUnit1049:TestMethod|}() {
await Task.Yield();
}
}";
var after = @"
using System.Threading.Tasks;
using Xunit;
public class TestClass {
[Fact]
public async ValueTask TestMethod() {
await Task.Yield();
}
}";

await Verify.VerifyCodeFixV3(before, after, DoNotUseAsyncVoidForTestMethodsFixer.Key_ConvertToValueTask);
}
}
23 changes: 23 additions & 0 deletions src/xunit.analyzers/Utility/CodeAnalysisExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ static bool IsTestClassStrict(
);
}

public static bool IsTestMethod(
this IMethodSymbol method,
XunitContext xunitContext,
ITypeSymbol attributeUsageType,
bool strict)
{
Guard.ArgumentNotNull(method);
Guard.ArgumentNotNull(xunitContext);

var factAttributeType = xunitContext.Core.FactAttributeType;
var theoryAttributeType = xunitContext.Core.TheoryAttributeType;
if (factAttributeType is null || theoryAttributeType is null)
return false;

var attributes = method.GetAttributesWithInheritance(attributeUsageType);
var comparer = SymbolEqualityComparer.Default;

return
strict
? attributes.Any(a => comparer.Equals(a.AttributeClass, factAttributeType) || comparer.Equals(a.AttributeClass, theoryAttributeType))
: attributes.Any(a => factAttributeType.IsAssignableFrom(a.AttributeClass));
}

public static IOperation WalkDownImplicitConversions(this IOperation operation)
{
Guard.ArgumentNotNull(operation);
Expand Down
18 changes: 16 additions & 2 deletions src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,23 @@ public static partial class Descriptors
"The argument '{0}' of type '{1}' might not be serializable, which may cause Test Explorer to not enumerate individual data rows. Consider using a value that is known to be serializable."
);

// Placeholder for rule X1048
public static DiagnosticDescriptor X1048_DoNotUseAsyncVoidForTestMethods_V2 { get; } =
Diagnostic(
"xUnit1048",
"Avoid using 'async void' for test methods as it is deprecated in xUnit.net v3",
Usage,
Warning,
"Support for 'async void' unit tests has been removed from xUnit.net v3. To simplify upgrading, convert the test to 'async Task' instead."
);

// Placeholder for rule X1049
public static DiagnosticDescriptor X1049_DoNotUseAsyncVoidForTestMethods_V3 { get; } =
Diagnostic(
"xUnit1049",
"Using 'async void' for test methods as it is deprecated in xUnit.net v3",
Usage,
Error,
"Support for 'async void' unit tests has been removed from xUnit.net v3. Convert the test to 'async Task' or 'async ValueTask' instead."
);

// Placeholder for rule X1050

Expand Down
55 changes: 55 additions & 0 deletions src/xunit.analyzers/X1000/DoNotUseAsyncVoidForTestMethods.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Xunit.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DoNotUseAsyncVoidForTestMethods : XunitDiagnosticAnalyzer
{
public DoNotUseAsyncVoidForTestMethods() :
base([
Descriptors.X1048_DoNotUseAsyncVoidForTestMethods_V2,
Descriptors.X1049_DoNotUseAsyncVoidForTestMethods_V3,
])
{ }

public override void AnalyzeCompilation(
CompilationStartAnalysisContext context,
XunitContext xunitContext)
{
Guard.ArgumentNotNull(context);
Guard.ArgumentNotNull(xunitContext);

var attributeUsageType = TypeSymbolFactory.AttributeUsageAttribute(context.Compilation);
if (attributeUsageType is null)
return;

context.RegisterSymbolAction(context =>
{
if (context.Symbol is not IMethodSymbol method)
return;

if (!method.IsTestMethod(xunitContext, attributeUsageType, strict: true))
return;

if (!method.ReturnsVoid)
return;

var location = context.Symbol.Locations.FirstOrDefault();
if (location is null)
return;

var propertiesBuilder = ImmutableDictionary.CreateBuilder<string, string?>();
propertiesBuilder.Add(Constants.Properties.DeclaringType, method.ContainingType.ToDisplayString());
propertiesBuilder.Add(Constants.Properties.MemberName, method.Name);
var properties = propertiesBuilder.ToImmutableDictionary();

if (xunitContext.HasV3References)
context.ReportDiagnostic(Diagnostic.Create(Descriptors.X1049_DoNotUseAsyncVoidForTestMethods_V3, location, properties));
else
context.ReportDiagnostic(Diagnostic.Create(Descriptors.X1048_DoNotUseAsyncVoidForTestMethods_V2, location, properties));
}, SymbolKind.Method);
}
}

0 comments on commit 2397d14

Please sign in to comment.