diff --git a/AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/Naming/UseAsyncSuffixUnitTests.cs b/AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/Naming/UseAsyncSuffixUnitTests.cs index c1ee24d..37d6dbf 100644 --- a/AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/Naming/UseAsyncSuffixUnitTests.cs +++ b/AsyncUsageAnalyzers/AsyncUsageAnalyzers.Test/Naming/UseAsyncSuffixUnitTests.cs @@ -385,6 +385,51 @@ class ClassName await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); } + [Theory] + [InlineData("static Task Main()")] + [InlineData("static Task Main()")] + [InlineData("static Task Main(string[] args)")] + [InlineData("static Task Main(params string[] args)")] + [InlineData("static Task Main(string[] args)")] + [InlineData("static Task Main(params string[] args)")] + [InlineData("public static Task Main(string[] args)")] + [InlineData("public static Task Main(params string[] args)")] + public async Task TestAsyncMainAsync(string signature) + { + string testCode = $@" +using System.Threading.Tasks; +class ClassName +{{ + {signature} {{ throw null; }} +}} +"; + + await this.VerifyCSharpDiagnosticAsync(testCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + } + + [Theory] + [InlineData(" Task Main()")] + [InlineData("static Task Main()")] + [InlineData("static Task Main(string[] args, CancellationToken cancellationToken)")] + [InlineData("static Task Main(string args)")] + public async Task TestAsyncMainNonMatchingSignatureAsync(string signature) + { + string testCode = $@" +using System.Threading; +using System.Threading.Tasks; +class ClassName +{{ + {signature} {{ throw null; }} +}} +"; + string fixedCode = testCode.Replace("Main", "MainAsync"); + + DiagnosticResult expected = this.CSharpDiagnostic().WithArguments("Main").WithLocation(6, 23); + await this.VerifyCSharpDiagnosticAsync(testCode, expected, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpDiagnosticAsync(fixedCode, EmptyDiagnosticResults, CancellationToken.None).ConfigureAwait(false); + await this.VerifyCSharpFixAsync(testCode, fixedCode, cancellationToken: CancellationToken.None).ConfigureAwait(false); + } + protected override IEnumerable GetCSharpDiagnosticAnalyzers() { yield return new UseAsyncSuffixAnalyzer(); diff --git a/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Helpers/MethodSymbolExtensions.cs b/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Helpers/MethodSymbolExtensions.cs index b3d10f8..34dee75 100644 --- a/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Helpers/MethodSymbolExtensions.cs +++ b/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Helpers/MethodSymbolExtensions.cs @@ -9,7 +9,7 @@ namespace AsyncUsageAnalyzers.Helpers internal static class MethodSymbolExtensions { - public static bool HasAsyncSignature(this IMethodSymbol symbol, bool treatAsyncVoidAsAsync = false) + public static bool HasAsyncSignature(this IMethodSymbol symbol, bool treatAsyncVoidAsAsync = false, bool treatValueTaskAsAsync = true) { // void-returning methods are not asynchronous according to their signature, even if they use `async` if (symbol.ReturnsVoid) @@ -26,7 +26,7 @@ public static bool HasAsyncSignature(this IMethodSymbol symbol, bool treatAsyncV { // This check conveniently covers Task and Task by ignoring the `1 in Task. if (!string.Equals(nameof(Task), symbol.ReturnType?.Name, StringComparison.Ordinal) - && !string.Equals("ValueTask", symbol.ReturnType?.Name, StringComparison.Ordinal)) + && !(treatValueTaskAsAsync && string.Equals("ValueTask", symbol.ReturnType?.Name, StringComparison.Ordinal))) { return false; } @@ -92,5 +92,52 @@ public static bool IsOverrideOrImplementation(this IMethodSymbol symbol) return false; } + + public static bool IsAsyncMain(this IMethodSymbol symbol) + { + // The following signatures are allowed: + // + // static Task Main() + // static Task Main() + // static Task Main(string[]) + // static Task Main(string[]) + if (!symbol.IsStatic) + { + return false; + } + + if (!string.Equals(symbol.Name, "Main", StringComparison.Ordinal)) + { + return false; + } + + if (!symbol.HasAsyncSignature(treatAsyncVoidAsAsync: false, treatValueTaskAsAsync: false)) + { + return false; + } + + var returnType = (INamedTypeSymbol)symbol.ReturnType; + if (returnType.IsGenericType) + { + if (returnType.TypeArguments.Length != 1 + || returnType.TypeArguments[0].SpecialType != SpecialType.System_Int32) + { + return false; + } + } + + switch (symbol.Parameters.Length) + { + case 0: + return true; + + case 1: + return symbol.Parameters[0].Type is IArrayTypeSymbol arrayType + && arrayType.ElementType.SpecialType == SpecialType.System_String; + + default: + return false; + } + } } } diff --git a/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/UseAsyncSuffixAnalyzer.cs b/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/UseAsyncSuffixAnalyzer.cs index 9ff1b85..c4aa91b 100644 --- a/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/UseAsyncSuffixAnalyzer.cs +++ b/AsyncUsageAnalyzers/AsyncUsageAnalyzers/Naming/UseAsyncSuffixAnalyzer.cs @@ -67,6 +67,11 @@ private static void HandleMethodDeclaration(SymbolAnalysisContext context) return; } + if (symbol.IsAsyncMain()) + { + return; + } + context.ReportDiagnostic(Diagnostic.Create(Descriptor, symbol.Locations[0], symbol.Name)); } }