diff --git a/TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs b/TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs index b514d349e4..50eb5a29b4 100644 --- a/TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/AsyncVoidAnalyzerTests.cs @@ -47,4 +47,145 @@ public async Task Test() """ ); } + + [Test] + public async Task Async_Void_Lambda_Raises_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Threading.Tasks; + using TUnit.Core; + + public class MyClass + { + [Test] + public void Test() + { + Action action = {|#0:async () => + { + await Task.Delay(1); + }|}; + } + } + """, + + Verifier + .Diagnostic(Rules.AsyncVoidMethod) + .WithLocation(0) + ); + } + + [Test] + public async Task Async_Task_Lambda_Raises_No_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Threading.Tasks; + using TUnit.Core; + + public class MyClass + { + [Test] + public void Test() + { + Func action = async () => + { + await Task.Delay(1); + }; + } + } + """ + ); + } + + [Test] + public async Task Async_Void_AnonymousDelegate_Raises_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Threading.Tasks; + using TUnit.Core; + + public class MyClass + { + [Test] + public void Test() + { + Action action = {|#0:async delegate + { + await Task.Delay(1); + }|}; + } + } + """, + + Verifier + .Diagnostic(Rules.AsyncVoidMethod) + .WithLocation(0) + ); + } + + [Test] + public async Task Async_Void_Lambda_With_Parameter_Raises_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Threading.Tasks; + using TUnit.Core; + + public class MyClass + { + [Test] + public void Test() + { + Action action = {|#0:async (x) => + { + await Task.Delay(1); + }|}; + } + } + """, + + Verifier + .Diagnostic(Rules.AsyncVoidMethod) + .WithLocation(0) + ); + } + + [Test] + public async Task Async_Void_SimpleLambda_Raises_Error() + { + await Verifier + .VerifyAnalyzerAsync( + """ + using System; + using System.Threading.Tasks; + using TUnit.Core; + + public class MyClass + { + [Test] + public void Test() + { + Action action = {|#0:async x => + { + await Task.Delay(1); + }|}; + } + } + """, + + Verifier + .Diagnostic(Rules.AsyncVoidMethod) + .WithLocation(0) + ); + } } diff --git a/TUnit.Analyzers/AnalyzerReleases.Shipped.md b/TUnit.Analyzers/AnalyzerReleases.Shipped.md index 7438273863..c5a5189fa0 100644 --- a/TUnit.Analyzers/AnalyzerReleases.Shipped.md +++ b/TUnit.Analyzers/AnalyzerReleases.Shipped.md @@ -62,7 +62,7 @@ TUnit0033 | Usage | Error | Circular or conflicting test dependencies detected Rule ID | Category | Severity | Notes --------|----------|----------|------------------------------------------------ TUnit0015 | Usage | Error | Methods with [Timeout] must have a CancellationToken parameter -TUnit0031 | Usage | Error | Async void methods not allowed - return Task instead +TUnit0031 | Usage | Error | Async void methods and lambdas not allowed - return Task instead #### AOT Compatibility Rules Rule ID | Category | Severity | Notes diff --git a/TUnit.Analyzers/AsyncVoidAnalyzer.cs b/TUnit.Analyzers/AsyncVoidAnalyzer.cs index 5be2edc6f9..c553c4b8b6 100644 --- a/TUnit.Analyzers/AsyncVoidAnalyzer.cs +++ b/TUnit.Analyzers/AsyncVoidAnalyzer.cs @@ -1,5 +1,7 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; namespace TUnit.Analyzers; @@ -12,10 +14,14 @@ public class AsyncVoidAnalyzer : ConcurrentDiagnosticAnalyzer protected override void InitializeInternal(AnalysisContext context) { - context.RegisterSymbolAction(AnalyzeSyntax, SymbolKind.Method); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + context.RegisterSyntaxNodeAction(AnalyzeLambda, + SyntaxKind.ParenthesizedLambdaExpression, + SyntaxKind.SimpleLambdaExpression, + SyntaxKind.AnonymousMethodExpression); } - private void AnalyzeSyntax(SymbolAnalysisContext context) + private void AnalyzeMethod(SymbolAnalysisContext context) { if (context.Symbol is not IMethodSymbol methodSymbol) { @@ -29,4 +35,26 @@ private void AnalyzeSyntax(SymbolAnalysisContext context) ); } } + + private void AnalyzeLambda(SyntaxNodeAnalysisContext context) + { + if (context.Node is not AnonymousFunctionExpressionSyntax anonymousFunction) + { + return; + } + + if (!anonymousFunction.Modifiers.Any(SyntaxKind.AsyncKeyword)) + { + return; + } + + var symbol = context.SemanticModel.GetSymbolInfo(anonymousFunction).Symbol; + + if (symbol is IMethodSymbol { ReturnsVoid: true }) + { + context.ReportDiagnostic(Diagnostic.Create(Rules.AsyncVoidMethod, + anonymousFunction.GetLocation()) + ); + } + } } diff --git a/TUnit.Analyzers/Resources.Designer.cs b/TUnit.Analyzers/Resources.Designer.cs index f8dab6bd94..098084a750 100644 --- a/TUnit.Analyzers/Resources.Designer.cs +++ b/TUnit.Analyzers/Resources.Designer.cs @@ -867,7 +867,7 @@ internal static string TUnit0030Title { } /// - /// Looks up a localized string similar to Async void methods are not allowed.. + /// Looks up a localized string similar to Async void methods and lambdas are not allowed.. /// internal static string TUnit0031Description { get { @@ -876,7 +876,7 @@ internal static string TUnit0031Description { } /// - /// Looks up a localized string similar to Async void methods are not allowed. + /// Looks up a localized string similar to Async void methods and lambdas are not allowed. /// internal static string TUnit0031MessageFormat { get { @@ -885,7 +885,7 @@ internal static string TUnit0031MessageFormat { } /// - /// Looks up a localized string similar to Async void methods are not allowed. + /// Looks up a localized string similar to Async void methods and lambdas are not allowed. /// internal static string TUnit0031Title { get { diff --git a/TUnit.Analyzers/Resources.resx b/TUnit.Analyzers/Resources.resx index ba705c6075..51720d75db 100644 --- a/TUnit.Analyzers/Resources.resx +++ b/TUnit.Analyzers/Resources.resx @@ -217,13 +217,13 @@ Class doesn't pick up tests from the base class - Async void methods are not allowed. + Async void methods and lambdas are not allowed. - Async void methods are not allowed + Async void methods and lambdas are not allowed - Async void methods are not allowed + Async void methods and lambdas are not allowed Conflicting DependsOn and NotInParallel attributes.