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 @@
-