Skip to content

Commit d99d64b

Browse files
authored
Merge pull request #1465 from microsoft/copilot/fix-1464
Add AdditionalFiles support to VSTHRD103 analyzer for excluding specific APIs
1 parent b793c49 commit d99d64b

File tree

6 files changed

+108
-11
lines changed

6 files changed

+108
-11
lines changed

docfx/analyzers/VSTHRD103.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ async Task DoAsync()
2727
await file.ReadAsync(buffer, 0, 10);
2828
}
2929
```
30+
31+
## Configuration
32+
33+
This analyzer can be configured to exclude specific APIs from generating diagnostics.
34+
Some APIs may have async versions that are less efficient or inappropriate for certain use cases.
35+
36+
See our [configuration](configuration.md) topic to learn how to exclude specific methods
37+
using the `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` file.
38+
```

docfx/analyzers/configuration.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,16 @@ thread.
7979
**Line format:** `[Namespace.TypeName]::MethodName`
8080

8181
**Sample:** `[System.Windows.Threading.Dispatcher]::Invoke`
82+
83+
## Methods to exclude from VSTHRD103 checks
84+
85+
The VSTHRD103 analyzer flags calls to synchronous methods where asynchronous equivalents exist,
86+
when in an async context. Sometimes certain APIs have async versions but those async versions
87+
are significantly slower, less efficient, or simply not preferred. These methods can be
88+
excluded from VSTHRD103 analysis by specifying them in a configuration file.
89+
90+
**Filename:** `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt`
91+
92+
**Line format:** `[Namespace.TypeName]::MethodName`
93+
94+
**Sample:** `[System.Data.SqlClient.SqlDataReader]::Read`

src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,39 @@ public override void Initialize(AnalysisContext context)
6868
context.EnableConcurrentExecution();
6969
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);
7070

71-
context.RegisterCodeBlockStartAction<SyntaxKind>(ctxt =>
71+
context.RegisterCompilationStartAction(compilationStartContext =>
7272
{
73-
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression);
74-
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression);
75-
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeConditionalAccessExpression), SyntaxKind.ConditionalAccessExpression);
73+
var excludedMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForSyncMethodsToExcludeFromVSTHRD103, compilationStartContext.CancellationToken).ToImmutableArray();
74+
75+
compilationStartContext.RegisterCodeBlockStartAction<SyntaxKind>(ctxt =>
76+
{
77+
var methodAnalyzer = new MethodAnalyzer(excludedMethods);
78+
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression);
79+
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression);
80+
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeConditionalAccessExpression), SyntaxKind.ConditionalAccessExpression);
81+
});
7682
});
7783
}
7884

7985
private class MethodAnalyzer
8086
{
81-
internal static void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context)
87+
private readonly ImmutableArray<CommonInterest.QualifiedMember> excludedMethods;
88+
89+
public MethodAnalyzer(ImmutableArray<CommonInterest.QualifiedMember> excludedMethods)
90+
{
91+
this.excludedMethods = excludedMethods;
92+
}
93+
94+
internal void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context)
8295
{
8396
var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node;
8497
if (IsInTaskReturningMethodOrDelegate(context))
8598
{
86-
InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingProperties);
99+
this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingProperties);
87100
}
88101
}
89102

90-
internal static void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContext context)
103+
internal void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContext context)
91104
{
92105
var conditionalAccessSyntax = (ConditionalAccessExpressionSyntax)context.Node;
93106
if (IsInTaskReturningMethodOrDelegate(context))
@@ -97,17 +110,17 @@ internal static void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContex
97110
MemberBindingExpressionSyntax bindingExpr => bindingExpr.Name,
98111
_ => conditionalAccessSyntax.WhenNotNull,
99112
};
100-
InspectMemberAccess(context, rightSide, CommonInterest.SyncBlockingProperties);
113+
this.InspectMemberAccess(context, rightSide, CommonInterest.SyncBlockingProperties);
101114
}
102115
}
103116

104-
internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
117+
internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
105118
{
106119
if (IsInTaskReturningMethodOrDelegate(context))
107120
{
108121
var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node;
109122
var memberAccessSyntax = invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax;
110-
if (memberAccessSyntax is not null && InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingMethods))
123+
if (memberAccessSyntax is not null && this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingMethods))
111124
{
112125
// Don't return double-diagnostics.
113126
return;
@@ -134,6 +147,12 @@ internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
134147
&& m.Name != invocationDeclaringMethod?.Identifier.Text
135148
&& m.HasAsyncCompatibleReturnType())
136149
{
150+
// Check if this method is excluded from VSTHRD103 diagnostics
151+
if (this.excludedMethods.Contains(methodSymbol))
152+
{
153+
return;
154+
}
155+
137156
// An async alternative exists.
138157
ImmutableDictionary<string, string?>? properties = ImmutableDictionary<string, string?>.Empty
139158
.Add(AsyncMethodKeyName, asyncMethodName);
@@ -197,7 +216,7 @@ private static bool IsInTaskReturningMethodOrDelegate(SyntaxNodeAnalysisContext
197216
return methodSymbol?.HasAsyncCompatibleReturnType() is true;
198217
}
199218

200-
private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable<CommonInterest.SyncBlockingMethod> problematicMethods)
219+
private bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable<CommonInterest.SyncBlockingMethod> problematicMethods)
201220
{
202221
ISymbol? memberSymbol = context.SemanticModel.GetSymbolInfo(memberName, context.CancellationToken).Symbol;
203222
if (memberSymbol is object)
@@ -206,6 +225,12 @@ private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, Expre
206225
{
207226
if (item.Method.IsMatch(memberSymbol))
208227
{
228+
// Check if this method is excluded from VSTHRD103 diagnostics
229+
if (this.excludedMethods.Contains(memberSymbol))
230+
{
231+
return false;
232+
}
233+
209234
Location? location = memberName.GetLocation();
210235
ImmutableDictionary<string, string?>? properties = ImmutableDictionary<string, string?>.Empty
211236
.Add(ExtensionMethodNamespaceKeyName, item.ExtensionMethodNamespace is object ? string.Join(".", item.ExtensionMethodNamespace) : string.Empty);

src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public static class CommonInterest
2525
public static readonly Regex FileNamePatternForMembersRequiringMainThread = new Regex(@"^vs-threading\.MembersRequiringMainThread(\..*)?.txt$", FileNamePatternRegexOptions);
2626
public static readonly Regex FileNamePatternForMethodsThatAssertMainThread = new Regex(@"^vs-threading\.MainThreadAssertingMethods(\..*)?.txt$", FileNamePatternRegexOptions);
2727
public static readonly Regex FileNamePatternForMethodsThatSwitchToMainThread = new Regex(@"^vs-threading\.MainThreadSwitchingMethods(\..*)?.txt$", FileNamePatternRegexOptions);
28+
public static readonly Regex FileNamePatternForSyncMethodsToExcludeFromVSTHRD103 = new Regex(@"^vs-threading\.SyncMethodsToExcludeFromVSTHRD103(\..*)?.txt$", FileNamePatternRegexOptions);
2829

2930
public static readonly IEnumerable<SyncBlockingMethod> JTFSyncBlockers = new[]
3031
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Test exclusions for VSTHRD103 analyzer
2+
[TestNamespace.TestClass]::SlowSyncMethod

test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,6 +1339,53 @@ void Bar() {}
13391339
await CSVerify.VerifyAnalyzerAsync(test);
13401340
}
13411341

1342+
[Fact]
1343+
public async Task SyncMethodCallInAsyncMethod_ExcludedViaAdditionalFiles_GeneratesNoWarning()
1344+
{
1345+
var test = @"
1346+
using System.Threading.Tasks;
1347+
1348+
class Test {
1349+
async Task T() {
1350+
TestNamespace.TestClass.SlowSyncMethod();
1351+
}
1352+
}
1353+
1354+
namespace TestNamespace {
1355+
class TestClass {
1356+
public static void SlowSyncMethod() { }
1357+
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
1358+
}
1359+
}
1360+
";
1361+
1362+
// No diagnostic expected because SlowSyncMethod is excluded via AdditionalFiles
1363+
await CSVerify.VerifyAnalyzerAsync(test);
1364+
}
1365+
1366+
[Fact]
1367+
public async Task SyncMethodCallInAsyncMethod_NotExcludedViaAdditionalFiles_GeneratesWarning()
1368+
{
1369+
var test = @"
1370+
using System.Threading.Tasks;
1371+
1372+
class Test {
1373+
async Task T() {
1374+
TestNamespace.TestClass.{|#0:NotExcludedMethod|}();
1375+
}
1376+
}
1377+
1378+
namespace TestNamespace {
1379+
class TestClass {
1380+
public static void NotExcludedMethod() { }
1381+
public static Task NotExcludedMethodAsync() => Task.CompletedTask;
1382+
}
1383+
}
1384+
";
1385+
1386+
await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("NotExcludedMethod", "NotExcludedMethodAsync"));
1387+
}
1388+
13421389
private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName)
13431390
=> CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(line, column, line, column + length).WithArguments(methodName);
13441391

0 commit comments

Comments
 (0)