diff --git a/docfx/analyzers/VSTHRD103.md b/docfx/analyzers/VSTHRD103.md index 348032057..120310cf3 100644 --- a/docfx/analyzers/VSTHRD103.md +++ b/docfx/analyzers/VSTHRD103.md @@ -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. +``` diff --git a/docfx/analyzers/configuration.md b/docfx/analyzers/configuration.md index 75d63c693..77acf4d82 100644 --- a/docfx/analyzers/configuration.md +++ b/docfx/analyzers/configuration.md @@ -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` diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs index 46cde9c5a..547ac2d81 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers.CSharp/VSTHRD103UseAsyncOptionAnalyzer.cs @@ -68,26 +68,39 @@ public override void Initialize(AnalysisContext context) context.EnableConcurrentExecution(); context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze); - context.RegisterCodeBlockStartAction(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(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 excludedMethods; + + public MethodAnalyzer(ImmutableArray 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)) @@ -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; @@ -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? properties = ImmutableDictionary.Empty .Add(AsyncMethodKeyName, asyncMethodName); @@ -197,7 +216,7 @@ private static bool IsInTaskReturningMethodOrDelegate(SyntaxNodeAnalysisContext return methodSymbol?.HasAsyncCompatibleReturnType() is true; } - private static bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable problematicMethods) + private bool InspectMemberAccess(SyntaxNodeAnalysisContext context, ExpressionSyntax memberName, IEnumerable problematicMethods) { ISymbol? memberSymbol = context.SemanticModel.GetSymbolInfo(memberName, context.CancellationToken).Symbol; if (memberSymbol is object) @@ -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? properties = ImmutableDictionary.Empty .Add(ExtensionMethodNamespaceKeyName, item.ExtensionMethodNamespace is object ? string.Join(".", item.ExtensionMethodNamespace) : string.Empty); diff --git a/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs index ef21f9f90..82e17d2e5 100644 --- a/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs +++ b/src/Microsoft.VisualStudio.Threading.Analyzers/CommonInterest.cs @@ -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 JTFSyncBlockers = new[] { diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt new file mode 100644 index 000000000..491c94c55 --- /dev/null +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/AdditionalFiles/vs-threading.SyncMethodsToExcludeFromVSTHRD103.mocks.txt @@ -0,0 +1,2 @@ +# Test exclusions for VSTHRD103 analyzer +[TestNamespace.TestClass]::SlowSyncMethod \ No newline at end of file diff --git a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs index b4ff2da62..3e6413088 100644 --- a/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs +++ b/test/Microsoft.VisualStudio.Threading.Analyzers.Tests/VSTHRD103UseAsyncOptionAnalyzerTests.cs @@ -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);