diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs index a478e12b..2578f59b 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs @@ -132,9 +132,19 @@ internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context) !methodSymbol.HasAsyncCompatibleReturnType()) { string asyncMethodName = methodSymbol.Name + VSTHRD200UseAsyncNamingConventionAnalyzer.MandatoryAsyncSuffix; + + // For reduced extension methods (invoked as instance.Method()), look up the async + // alternative on the receiver type so that extension methods defined in a separate + // static class (but applicable to the receiver) are found via includeReducedExtensionMethods. + // LookupSymbols with the static declaring class as container does not return extension + // methods applicable to the receiver type. + INamespaceOrTypeSymbol lookupContainer = methodSymbol.ReducedFrom is { } reducedFrom && reducedFrom.Parameters.Length > 0 + ? (INamespaceOrTypeSymbol)reducedFrom.Parameters[0].Type + : methodSymbol.ContainingType; + ImmutableArray symbols = context.SemanticModel.LookupSymbols( invocationExpressionSyntax.Expression.GetLocation().SourceSpan.Start, - methodSymbol.ContainingType, + lookupContainer, asyncMethodName, includeReducedExtensionMethods: true); diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs index cdeb7358..0e27c2c5 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs @@ -1410,6 +1410,55 @@ async Task BarAsync(int id) { await CSVerify.VerifyAnalyzerAsync(test); } + [Fact] + public async Task SyncExtensionMethodWhereAsyncAlternativeExistsInSameStaticClassGeneratesWarning() + { + var test = @" +using System.Threading.Tasks; + +public interface IExecutable { } + +public static class ExecutableExtensions +{ + public static string GetOutput(this IExecutable executable) => """"; + public static Task GetOutputAsync(this IExecutable executable) => Task.FromResult(""""); +} + +class Test +{ + async Task DoWorkAsync() + { + IExecutable exec = null!; + string result = exec.{|#0:GetOutput|}(); + } +} +"; + + var withFix = @" +using System.Threading.Tasks; + +public interface IExecutable { } + +public static class ExecutableExtensions +{ + public static string GetOutput(this IExecutable executable) => """"; + public static Task GetOutputAsync(this IExecutable executable) => Task.FromResult(""""); +} + +class Test +{ + async Task DoWorkAsync() + { + IExecutable exec = null!; + string result = await exec.GetOutputAsync(); + } +} +"; + + DiagnosticResult expected = CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("GetOutput", "GetOutputAsync"); + await CSVerify.VerifyCodeFixAsync(test, expected, withFix); + } + private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName) => CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(line, column, line, column + length).WithArguments(methodName);