Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
118 changes: 118 additions & 0 deletions TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,122 @@ 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_Lambda_In_Local_Function_In_Test_Raises_Error()
{
// A local function is not a boundary: a lambda nested inside one within a test
// is still scoped to the test and must be flagged. See #6190 review.
await Verifier
.VerifyAnalyzerAsync(
"""
using System;
using System.Threading.Tasks;
using TUnit.Core;

public class MyClass
{
[Test]
public void Test()
{
void Helper()
{
Action action = {|#0:async () =>
{
await Task.Delay(1);
}|};
}

Helper();
}
}
""",

Verifier
.Diagnostic(Rules.AsyncVoidMethod)
.WithLocation(0)
);
}

[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)
);
}
}
64 changes: 55 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,46 @@ 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, and local functions to the
// real enclosing method, so a lambda nested inside a local function of a test
// is still scoped to that test rather than treating the local function as a boundary.
while (symbol is IMethodSymbol method)
{
if (method.MethodKind is not (MethodKind.AnonymousFunction or MethodKind.LocalFunction))
{
return method;
}

symbol = method.ContainingSymbol;
}

return null;
}
}
Loading