From 29bc2fa339f1930dfce5c3ed0b71ca6915a10a26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:14:59 +0000 Subject: [PATCH 1/3] Initial plan From 215573be51ec66a57fd8944e4f9513a4ebd5a49e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:29:49 +0000 Subject: [PATCH 2/3] Fix VSTHRD103 to detect sync extension methods when async alternative exists in same static class Agent-Logs-Url: https://github.com/microsoft/vs-threading/sessions/d0908deb-1f03-4a2b-8c97-4c4530598e07 Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com> --- nuget.config | 1 + .../VSTHRD103UseAsyncOptionAnalyzer.cs | 12 ++++- .../VSTHRD103UseAsyncOptionAnalyzerTests.cs | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/nuget.config b/nuget.config index 35d15c110..a9269e594 100644 --- a/nuget.config +++ b/nuget.config @@ -6,6 +6,7 @@ + diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs index a478e12bb..2578f59b8 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 cdeb73583..0e27c2c55 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); From 3f6ea70b418b86e4f2c37dcb4af47daeb8c2228c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:30:04 +0000 Subject: [PATCH 3/3] Revert accidental nuget.config change (local package source for testing only) Agent-Logs-Url: https://github.com/microsoft/vs-threading/sessions/d0908deb-1f03-4a2b-8c97-4c4530598e07 Co-authored-by: drewnoakes <350947+drewnoakes@users.noreply.github.com> --- nuget.config | 1 - 1 file changed, 1 deletion(-) diff --git a/nuget.config b/nuget.config index a9269e594..35d15c110 100644 --- a/nuget.config +++ b/nuget.config @@ -6,7 +6,6 @@ -