Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,86 @@ public void Test()
.WithLocation(0)
);
}

[Test]
public async Task Async_Void_NonTest_Method_Raises_No_Error()
{
// An async void method that is not a test or hook (e.g. an event handler)
// must not be flagged just because the project references TUnit. See #6190.
await Verifier
.VerifyAnalyzerAsync(
"""
using System;
using System.Threading.Tasks;
using TUnit.Core;

public class MyClass
{
public event EventHandler? Ticked;

public void Setup()
{
Ticked += OnTicked;
}

private async void OnTicked(object? sender, EventArgs e)
{
await Task.Delay(1);
}
}
"""
);
}

[Test]
public async Task Async_Void_EventHandler_Lambda_In_NonTest_Method_Raises_No_Error()
{
// An async void lambda subscribed to an event from a non-test method is legitimate.
await Verifier
.VerifyAnalyzerAsync(
"""
using System;
using System.Threading.Tasks;
using TUnit.Core;

public class MyClass
{
public event EventHandler? Ticked;

public void Setup()
{
Ticked += async (sender, e) =>
{
await Task.Delay(1);
};
}
}
"""
);
}

[Test]
public async Task Async_Void_Hook_Raises_Error()
{
await Verifier
.VerifyAnalyzerAsync(
"""
using System.Threading.Tasks;
using TUnit.Core;

public class MyClass
{
[Before(HookType.Test)]
public async void {|#0:Setup|}()
{
await Task.Delay(1);
}
}
""",

Verifier
.Diagnostic(Rules.AsyncVoidMethod)
.WithLocation(0)
);
}
}
62 changes: 53 additions & 9 deletions TUnit.Analyzers/AsyncVoidAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using System.Collections.Immutable;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using TUnit.Analyzers.Extensions;

namespace TUnit.Analyzers;

Expand All @@ -28,12 +29,22 @@ private void AnalyzeMethod(SymbolAnalysisContext context)
return;
}

if (methodSymbol is { IsAsync: true, ReturnsVoid: true })
if (methodSymbol is not { IsAsync: true, ReturnsVoid: true })
{
context.ReportDiagnostic(Diagnostic.Create(Rules.AsyncVoidMethod,
methodSymbol.Locations.FirstOrDefault())
);
return;
}

// Only flag `async void` on TUnit test methods and hooks. Other `async void`
// methods (e.g. an event handler for System.Timers.Timer.Elapsed) are legitimate
// and must not be flagged just because the project references TUnit. See #6190.
if (!IsTestOrHookMethod(methodSymbol, context.Compilation))
{
return;
}

context.ReportDiagnostic(Diagnostic.Create(Rules.AsyncVoidMethod,
methodSymbol.Locations.FirstOrDefault())
);
}

private void AnalyzeLambda(SyntaxNodeAnalysisContext context)
Expand All @@ -50,11 +61,44 @@ private void AnalyzeLambda(SyntaxNodeAnalysisContext context)

var symbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol;

if (symbol is IMethodSymbol { ReturnsVoid: true })
if (symbol is not IMethodSymbol { ReturnsVoid: true })
{
context.ReportDiagnostic(Diagnostic.Create(Rules.AsyncVoidMethod,
anonymousFunction.GetLocation())
);
return;
}

// Only flag `async void` lambdas declared inside a TUnit test method or hook,
// so legitimate async void event-handler lambdas elsewhere aren't flagged. See #6190.
var enclosingMethod = GetEnclosingMethod(symbol);

if (enclosingMethod is null || !IsTestOrHookMethod(enclosingMethod, context.Compilation))
{
return;
}

context.ReportDiagnostic(Diagnostic.Create(Rules.AsyncVoidMethod,
anonymousFunction.GetLocation())
);
}

private static bool IsTestOrHookMethod(IMethodSymbol methodSymbol, Compilation compilation)
{
return methodSymbol.HasTestAttribute(compilation)
|| methodSymbol.IsHookMethod(compilation, out _, out _, out _);
}

private static IMethodSymbol? GetEnclosingMethod(ISymbol? symbol)
{
// Walk out of nested lambdas/anonymous methods to the real enclosing method.
while (symbol is IMethodSymbol method)
{
if (method.MethodKind != MethodKind.AnonymousFunction)
{
return method;
}

symbol = method.ContainingSymbol;
}

return null;
}
}
Loading