Skip to content
9 changes: 9 additions & 0 deletions docfx/analyzers/VSTHRD103.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ async Task DoAsync()
await file.ReadAsync(buffer, 0, 10);
}
```

## Configuration

This analyzer can be configured to exclude specific APIs from generating diagnostics.
Some APIs may have async versions that are less efficient or inappropriate for certain use cases.

See our [configuration](configuration.md) topic to learn how to exclude specific methods
using the `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt` file.
```
13 changes: 13 additions & 0 deletions docfx/analyzers/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,16 @@ thread.
**Line format:** `[Namespace.TypeName]::MethodName`

**Sample:** `[System.Windows.Threading.Dispatcher]::Invoke`

## Methods to exclude from VSTHRD103 checks

The VSTHRD103 analyzer flags calls to synchronous methods where asynchronous equivalents exist,
when in an async context. Sometimes certain APIs have async versions but those async versions
are significantly slower, less efficient, or simply not preferred. These methods can be
excluded from VSTHRD103 analysis by specifying them in a configuration file.

**Filename:** `vs-threading.SyncMethodsToExcludeFromVSTHRD103.txt`

**Line format:** `[Namespace.TypeName]::MethodName`

**Sample:** `[System.Data.SqlClient.SqlDataReader]::Read`
Original file line number Diff line number Diff line change
Expand Up @@ -68,26 +68,39 @@ public override void Initialize(AnalysisContext context)
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);

context.RegisterCodeBlockStartAction<SyntaxKind>(ctxt =>
context.RegisterCompilationStartAction(compilationStartContext =>
{
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression);
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression);
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(MethodAnalyzer.AnalyzeConditionalAccessExpression), SyntaxKind.ConditionalAccessExpression);
var excludedMethods = CommonInterest.ReadMethods(compilationStartContext.Options, CommonInterest.FileNamePatternForSyncMethodsToExcludeFromVSTHRD103, compilationStartContext.CancellationToken).ToImmutableArray();

compilationStartContext.RegisterCodeBlockStartAction<SyntaxKind>(ctxt =>
{
var methodAnalyzer = new MethodAnalyzer(excludedMethods);
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeInvocation), SyntaxKind.InvocationExpression);
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzePropertyGetter), SyntaxKind.SimpleMemberAccessExpression);
ctxt.RegisterSyntaxNodeAction(Utils.DebuggableWrapper(methodAnalyzer.AnalyzeConditionalAccessExpression), SyntaxKind.ConditionalAccessExpression);
});
});
}

private class MethodAnalyzer
{
internal static void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context)
private readonly ImmutableArray<CommonInterest.QualifiedMember> excludedMethods;

public MethodAnalyzer(ImmutableArray<CommonInterest.QualifiedMember> excludedMethods)
{
this.excludedMethods = excludedMethods;
}

internal void AnalyzePropertyGetter(SyntaxNodeAnalysisContext context)
{
var memberAccessSyntax = (MemberAccessExpressionSyntax)context.Node;
if (IsInTaskReturningMethodOrDelegate(context))
{
InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingProperties);
this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingProperties);
}
}

internal static void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContext context)
internal void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContext context)
{
var conditionalAccessSyntax = (ConditionalAccessExpressionSyntax)context.Node;
if (IsInTaskReturningMethodOrDelegate(context))
Expand All @@ -97,17 +110,17 @@ internal static void AnalyzeConditionalAccessExpression(SyntaxNodeAnalysisContex
MemberBindingExpressionSyntax bindingExpr => bindingExpr.Name,
_ => conditionalAccessSyntax.WhenNotNull,
};
InspectMemberAccess(context, rightSide, CommonInterest.SyncBlockingProperties);
this.InspectMemberAccess(context, rightSide, CommonInterest.SyncBlockingProperties);
}
}

internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
internal void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
{
if (IsInTaskReturningMethodOrDelegate(context))
{
var invocationExpressionSyntax = (InvocationExpressionSyntax)context.Node;
var memberAccessSyntax = invocationExpressionSyntax.Expression as MemberAccessExpressionSyntax;
if (memberAccessSyntax is not null && InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingMethods))
if (memberAccessSyntax is not null && this.InspectMemberAccess(context, memberAccessSyntax.Name, CommonInterest.SyncBlockingMethods))
{
// Don't return double-diagnostics.
return;
Expand All @@ -134,6 +147,12 @@ internal static void AnalyzeInvocation(SyntaxNodeAnalysisContext context)
&& m.Name != invocationDeclaringMethod?.Identifier.Text
&& m.HasAsyncCompatibleReturnType())
{
// Check if this method is excluded from VSTHRD103 diagnostics
if (this.excludedMethods.Contains(methodSymbol))
{
return;
}

// An async alternative exists.
ImmutableDictionary<string, string?>? properties = ImmutableDictionary<string, string?>.Empty
.Add(AsyncMethodKeyName, asyncMethodName);
Expand Down Expand Up @@ -197,7 +216,7 @@ private static bool IsInTaskReturningMethodOrDelegate(SyntaxNodeAnalysisContext
return methodSymbol?.HasAsyncCompatibleReturnType() is true;
}

private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable<CommonInterest.SyncBlockingMethod> problematicMethods)
private bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable<CommonInterest.SyncBlockingMethod> problematicMethods)
{
ISymbol? memberSymbol = context.SemanticModel.GetSymbolInfo(memberName, context.CancellationToken).Symbol;
if (memberSymbol is object)
Expand All @@ -206,6 +225,12 @@ private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, Expre
{
if (item.Method.IsMatch(memberSymbol))
{
// Check if this method is excluded from VSTHRD103 diagnostics
if (this.excludedMethods.Contains(memberSymbol))
{
return false;
}

Location? location = memberName.GetLocation();
ImmutableDictionary<string, string?>? properties = ImmutableDictionary<string, string?>.Empty
.Add(ExtensionMethodNamespaceKeyName, item.ExtensionMethodNamespace is object ? string.Join(".", item.ExtensionMethodNamespace) : string.Empty);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public static class CommonInterest
public static readonly Regex FileNamePatternForMembersRequiringMainThread = new Regex(@"^vs-threading\.MembersRequiringMainThread(\..*)?.txt$", FileNamePatternRegexOptions);
public static readonly Regex FileNamePatternForMethodsThatAssertMainThread = new Regex(@"^vs-threading\.MainThreadAssertingMethods(\..*)?.txt$", FileNamePatternRegexOptions);
public static readonly Regex FileNamePatternForMethodsThatSwitchToMainThread = new Regex(@"^vs-threading\.MainThreadSwitchingMethods(\..*)?.txt$", FileNamePatternRegexOptions);
public static readonly Regex FileNamePatternForSyncMethodsToExcludeFromVSTHRD103 = new Regex(@"^vs-threading\.SyncMethodsToExcludeFromVSTHRD103(\..*)?.txt$", FileNamePatternRegexOptions);

public static readonly IEnumerable<SyncBlockingMethod> JTFSyncBlockers = new[]
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Test exclusions for VSTHRD103 analyzer
[TestNamespace.TestClass]::SlowSyncMethod
Original file line number Diff line number Diff line change
Expand Up @@ -1339,6 +1339,53 @@ void Bar() {}
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task SyncMethodCallInAsyncMethod_ExcludedViaAdditionalFiles_GeneratesNoWarning()
{
var test = @"
using System.Threading.Tasks;

class Test {
async Task T() {
TestNamespace.TestClass.SlowSyncMethod();
}
}

namespace TestNamespace {
class TestClass {
public static void SlowSyncMethod() { }
public static Task SlowSyncMethodAsync() => Task.CompletedTask;
}
}
";

// No diagnostic expected because SlowSyncMethod is excluded via AdditionalFiles
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task SyncMethodCallInAsyncMethod_NotExcludedViaAdditionalFiles_GeneratesWarning()
{
var test = @"
using System.Threading.Tasks;

class Test {
async Task T() {
TestNamespace.TestClass.{|#0:NotExcludedMethod|}();
}
}

namespace TestNamespace {
class TestClass {
public static void NotExcludedMethod() { }
public static Task NotExcludedMethodAsync() => Task.CompletedTask;
}
}
";

await CSVerify.VerifyAnalyzerAsync(test, CSVerify.Diagnostic(Descriptor).WithLocation(0).WithArguments("NotExcludedMethod", "NotExcludedMethodAsync"));
}

private DiagnosticResult CreateDiagnostic(int line, int column, int length, string methodName)
=> CSVerify.Diagnostic(DescriptorNoAlternativeMethod).WithSpan(line, column, line, column + length).WithArguments(methodName);

Expand Down