diff --git a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs index 8c7d394a..9d7b2d67 100755 --- a/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs +++ b/src/Meziantou.Analyzer/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer.cs @@ -89,6 +89,8 @@ public Context(Compilation compilation) ValueTaskAwaiterOfTSymbol = compilation.GetBestTypeByMetadataName("System.Runtime.CompilerServices.ValueTaskAwaiter`1"); ThreadSymbol = compilation.GetBestTypeByMetadataName("System.Threading.Thread"); + SemaphoreSlimSymbol = compilation.GetBestTypeByMetadataName("System.Threading.SemaphoreSlim"); + TimeSpanSymbol = compilation.GetBestTypeByMetadataName("System.TimeSpan"); DbContextSymbol = compilation.GetBestTypeByMetadataName("Microsoft.EntityFrameworkCore.DbContext"); DbSetSymbol = compilation.GetBestTypeByMetadataName("Microsoft.EntityFrameworkCore.DbSet`1"); @@ -130,6 +132,8 @@ public Context(Compilation compilation) private INamedTypeSymbol? ValueTaskAwaiterOfTSymbol { get; } private INamedTypeSymbol? ThreadSymbol { get; } + private INamedTypeSymbol? SemaphoreSlimSymbol { get; } + private INamedTypeSymbol? TimeSpanSymbol { get; } private INamedTypeSymbol? DbContextSymbol { get; } private INamedTypeSymbol? DbSetSymbol { get; } @@ -175,20 +179,17 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(tru // Task.Wait() // Task`1.Wait() - else if (targetMethod.Name == nameof(Task.Wait)) + else if (targetMethod.Name == nameof(Task.Wait) && targetMethod.ContainingType.OriginalDefinition.IsEqualToAny(TaskSymbol, TaskOfTSymbol)) { - if (targetMethod.ContainingType.OriginalDefinition.IsEqualToAny(TaskSymbol, TaskOfTSymbol)) + if (operation.Arguments.Length == 0) { - if (operation.Arguments.Length == 0) - { - data = new("Use await instead of 'Wait()'", DoNotUseBlockingCallInAsyncContextData.Task_Wait); - return true; - } - else - { - data = new("Use 'WaitAsync' instead of 'Wait()'", DoNotUseBlockingCallInAsyncContextData.Task_Wait_Delay); - return true; - } + data = new("Use await instead of 'Wait()'", DoNotUseBlockingCallInAsyncContextData.Task_Wait); + return true; + } + else + { + data = new("Use 'WaitAsync' instead of 'Wait()'", DoNotUseBlockingCallInAsyncContextData.Task_Wait_Delay); + return true; } } @@ -240,6 +241,12 @@ private bool HasAsyncEquivalent(IInvocationOperation operation, [NotNullWhen(tru return false; } + // SemaphoreSlim.Wait(0) is a non-blocking try-acquire pattern, skip it + else if (SemaphoreSlimSymbol is not null && targetMethod.Name == "Wait" && targetMethod.ContainingType.IsEqualTo(SemaphoreSlimSymbol) && IsSemaphoreSlimWaitWithZeroTimeout(operation)) + { + return false; + } + // Search async equivalent: sample.Write() => sample.WriteAsync() if (!targetMethod.ReturnType.OriginalDefinition.IsEqualToAny(TaskSymbol, TaskOfTSymbol)) { @@ -305,6 +312,28 @@ private bool IsPotentialMember(IInvocationOperation operation, IMethodSymbol met return false; } + private bool IsSemaphoreSlimWaitWithZeroTimeout(IInvocationOperation operation) + { + if (operation.Arguments.Length == 0) + return false; + + var firstArgument = operation.Arguments[0]; + var constantValue = firstArgument.Value.ConstantValue; + + // Check for Wait(0) - integer literal 0 + if (constantValue.HasValue && constantValue.Value is int intValue && intValue == 0) + return true; + + // Check for Wait(TimeSpan.Zero) + if (TimeSpanSymbol is not null && firstArgument.Value is IMemberReferenceOperation memberRef) + { + if (memberRef.Member.Name == nameof(TimeSpan.Zero) && memberRef.Member.ContainingType.IsEqualTo(TimeSpanSymbol)) + return true; + } + + return false; + } + internal void AnalyzePropertyReference(OperationAnalysisContext context) { var operation = (IPropertyReferenceOperation)context.Operation; diff --git a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs index 0bfd6fa2..87f7f85f 100644 --- a/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs +++ b/tests/Meziantou.Analyzer.Test/Rules/DoNotUseBlockingCallInAsyncContextAnalyzer_AsyncContextTests.cs @@ -1014,4 +1014,100 @@ await CreateProjectBuilder() """) .ValidateAsync(); } + + [Fact] + public async Task SemaphoreSlim_Wait_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + class Test + { + public async Task A() + { + var semaphore = new SemaphoreSlim(1); + semaphore.Wait(0); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task SemaphoreSlim_Wait_TimeSpanZero_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System; + using System.Threading; + using System.Threading.Tasks; + class Test + { + public async Task A() + { + var semaphore = new SemaphoreSlim(1); + semaphore.Wait(TimeSpan.Zero); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task SemaphoreSlim_Wait_NonZero_Diagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + class Test + { + public async Task A() + { + var semaphore = new SemaphoreSlim(1); + [||]semaphore.Wait(100); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task SemaphoreSlim_Wait_NoArgs_Diagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + class Test + { + public async Task A() + { + var semaphore = new SemaphoreSlim(1); + [||]semaphore.Wait(); + } + } + """) + .ValidateAsync(); + } + + [Fact] + public async Task SemaphoreSlim_Wait_ZeroWithCancellationToken_NoDiagnostic() + { + await CreateProjectBuilder() + .WithSourceCode(""" + using System.Threading; + using System.Threading.Tasks; + class Test + { + public async Task A() + { + var semaphore = new SemaphoreSlim(1); + semaphore.Wait(0, CancellationToken.None); + } + } + """) + .ValidateAsync(); + } }