diff --git a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContext.cs b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContext.cs index a720990d51..1e29c872f5 100644 --- a/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContext.cs +++ b/src/NetAnalyzers/Core/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContext.cs @@ -4,7 +4,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; +using System.Threading; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis; @@ -83,6 +85,20 @@ public override void Initialize(AnalysisContext context) return; } + INamedTypeSymbol? semaphoreSlimType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingSemaphoreSlim); + INamedTypeSymbol? timeSpanType = wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemTimeSpan); + INamedTypeSymbol intType = context.Compilation.GetSpecialType(SpecialType.System_Int32); + IFieldSymbol? timeSpanZero = timeSpanType?.GetMembers(nameof(TimeSpan.Zero)) + .OfType() + .FirstOrDefault(); + ImmutableArray semaphoreSlimWaitWithTimeoutMethods = semaphoreSlimType + ?.GetMembers(nameof(SemaphoreSlim.Wait)) + .OfType() + .Where(m => m.Parameters.Length > 0 + && (SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, intType) + || SymbolEqualityComparer.Default.Equals(m.Parameters[0].Type, timeSpanType))) + .ToImmutableArray() ?? ImmutableArray.Empty; + ImmutableArray excludedMethods = GetExcludedMethods(wellKnownTypeProvider); context.RegisterOperationAction(context => { @@ -91,7 +107,9 @@ public override void Initialize(AnalysisContext context) if (context.Operation is IInvocationOperation invocationOperation) { var methodSymbol = invocationOperation.TargetMethod; - if (excludedMethods.Contains(methodSymbol.OriginalDefinition, SymbolEqualityComparer.Default) || InspectAndReportBlockingMemberAccess(context, methodSymbol, syncBlockingSymbols, SymbolKind.Method)) + if (excludedMethods.Contains(methodSymbol.OriginalDefinition, SymbolEqualityComparer.Default) + || InspectAndReportBlockingMemberAccess(context, methodSymbol, syncBlockingSymbols, SymbolKind.Method) + || IsSemaphoreSlimWaitWithZeroArgumentInvocation(invocationOperation, timeSpanZero, semaphoreSlimWaitWithTimeoutMethods)) { // Don't return double-diagnostics. return; @@ -148,6 +166,23 @@ public override void Initialize(AnalysisContext context) }); } + private static bool IsSemaphoreSlimWaitWithZeroArgumentInvocation(IInvocationOperation invocation, IFieldSymbol? timeSpanZero, ImmutableArray semaphoreSlimWaitWithTimeoutMethods) + { + if (!semaphoreSlimWaitWithTimeoutMethods.Contains(invocation.TargetMethod, SymbolEqualityComparer.Default)) + { + return false; + } + + Debug.Assert(!invocation.Arguments.IsEmpty); + + IOperation argumentValue = invocation.Arguments[0].Value; + + return argumentValue.HasConstantValue(0) + || timeSpanZero is not null + && argumentValue is IFieldReferenceOperation fieldReference + && SymbolEqualityComparer.Default.Equals(fieldReference.Field, timeSpanZero); + } + private static SymbolDisplayFormat GetLanguageSpecificFormat(IOperation operation) => operation.Language == LanguageNames.CSharp ? SymbolDisplayFormat.CSharpShortErrorMessageFormat : SymbolDisplayFormat.VisualBasicShortErrorMessageFormat; diff --git a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContextTests.cs b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContextTests.cs index f9e050ca07..10cadbcd90 100644 --- a/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContextTests.cs +++ b/src/NetAnalyzers/UnitTests/Microsoft.NetCore.Analyzers/Runtime/UseAsyncMethodInAsyncContextTests.cs @@ -1343,6 +1343,218 @@ public async Task Foo() return VerifyCS.VerifyAnalyzerAsync(code); } + [Fact, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + public Task WhenPassingZeroToSemaphoreSlimWait_NoDiagnostic() + { + const string code = """ + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + s.Wait(0); + } + } + """; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Fact, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + public Task WhenPassingZeroWithCancellationTokenToSemaphoreSlimWait_NoDiagnostic() + { + const string code = """ + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + s.Wait(0, CancellationToken.None); + } + } + """; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Fact, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + public Task WhenPassingTimeSpanZeroToSemaphoreSlimWait_NoDiagnostic() + { + const string code = """ + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + s.Wait(TimeSpan.Zero); + } + } + """; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Fact, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + public Task WhenPassingTimeSpanZeroWithCancellationTokenToSemaphoreSlimWait_NoDiagnostic() + { + const string code = """ + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + s.Wait(TimeSpan.Zero, CancellationToken.None); + } + } + """; + + return VerifyCS.VerifyAnalyzerAsync(code); + } + + [Theory, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + [InlineData("1")] + [InlineData("500")] + public Task WhenPassingNonZeroToSemaphoreSlimWait_Diagnostic(string nonZero) + { + var code = $$""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + {|#0:s.Wait({{nonZero}})|}; + } + } + """; + var result = new DiagnosticResult(UseAsyncMethodInAsyncContext.Descriptor) + .WithLocation(0) + .WithArguments("SemaphoreSlim.Wait(int)", "SemaphoreSlim.WaitAsync()"); + + return VerifyCS.VerifyAnalyzerAsync(code, result); + } + + [Theory, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + [InlineData("1")] + [InlineData("500")] + public Task WhenPassingNonZeroWithCancellationTokenToSemaphoreSlimWait_Diagnostic(string nonZero) + { + var code = $$""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + {|#0:s.Wait({{nonZero}}, CancellationToken.None)|}; + } + } + """; + var result = new DiagnosticResult(UseAsyncMethodInAsyncContext.Descriptor) + .WithLocation(0) + .WithArguments("SemaphoreSlim.Wait(int, CancellationToken)", "SemaphoreSlim.WaitAsync()"); + + return VerifyCS.VerifyAnalyzerAsync(code, result); + } + + [Theory, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + [InlineData("TimeSpan.FromSeconds(30)")] + [InlineData("TimeSpan.Parse(\"0:32:0\")")] + public Task WhenPassingNonZeroTimeSpanToSemaphoreSlimWait_Diagnostic(string nonZero) + { + var code = $$""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + {|#0:s.Wait({{nonZero}})|}; + } + } + """; + var result = new DiagnosticResult(UseAsyncMethodInAsyncContext.Descriptor) + .WithLocation(0) + .WithArguments("SemaphoreSlim.Wait(TimeSpan)", "SemaphoreSlim.WaitAsync()"); + + return VerifyCS.VerifyAnalyzerAsync(code, result); + } + + [Theory, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + [InlineData("TimeSpan.FromSeconds(30)")] + [InlineData("TimeSpan.Parse(\"0:32:0\")")] + public Task WhenPassingNonZeroTimeSpanWithCancellationTokenToSemaphoreSlimWait_Diagnostic(string nonZero) + { + var code = $$""" + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + {|#0:s.Wait({{nonZero}}, CancellationToken.None)|}; + } + } + """; + var result = new DiagnosticResult(UseAsyncMethodInAsyncContext.Descriptor) + .WithLocation(0) + .WithArguments("SemaphoreSlim.Wait(TimeSpan, CancellationToken)", "SemaphoreSlim.WaitAsync()"); + + return VerifyCS.VerifyAnalyzerAsync(code, result); + } + + [Fact, WorkItem(7271, "https://github.com/dotnet/roslyn-analyzers/issues/7271")] + public Task WhenPassingCancellationTokenToSemaphoreSlimWait_Diagnostic() + { + const string code = """ + using System; + using System.Threading; + using System.Threading.Tasks; + + class Test + { + async Task M() + { + SemaphoreSlim s = new SemaphoreSlim(0); + {|#0:s.Wait(CancellationToken.None)|}; + } + } + """; + var result = new DiagnosticResult(UseAsyncMethodInAsyncContext.Descriptor) + .WithLocation(0) + .WithArguments("SemaphoreSlim.Wait(CancellationToken)", "SemaphoreSlim.WaitAsync()"); + + return VerifyCS.VerifyAnalyzerAsync(code, result); + } + private static async Task CreateCSTestAndRunAsync(string testCS) { var csTestVerify = new VerifyCS.Test diff --git a/src/Utilities/Compiler/WellKnownTypeNames.cs b/src/Utilities/Compiler/WellKnownTypeNames.cs index 9fa8662c79..304ae70813 100644 --- a/src/Utilities/Compiler/WellKnownTypeNames.cs +++ b/src/Utilities/Compiler/WellKnownTypeNames.cs @@ -426,6 +426,7 @@ internal static class WellKnownTypeNames public const string SystemThreadingCancellationToken = "System.Threading.CancellationToken"; public const string SystemThreadingInterlocked = "System.Threading.Interlocked"; public const string SystemThreadingMonitor = "System.Threading.Monitor"; + public const string SystemThreadingSemaphoreSlim = "System.Threading.SemaphoreSlim"; public const string SystemThreadingSpinLock = "System.Threading.SpinLock"; public const string SystemThreadingTasksConfigureAwaitOptions = "System.Threading.Tasks.ConfigureAwaitOptions"; public const string SystemThreadingTasksTaskAsyncEnumerableExtensions = "System.Threading.Tasks.TaskAsyncEnumerableExtensions";