@@ -2304,6 +2304,29 @@ or ImplicitObjectCreationExpressionSyntax
23042304 && ThisFieldName ( a . Left ) is { } cfn ) // THIS instance's field only — `other._c` must not exempt our field (CodeRabbit)
23052305 eventSourceCounters . Add ( cfn ) ;
23062306
2307+ // P-004 SemaphoreSlim field exemption (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore).
2308+ // SemaphoreSlim.Dispose() only frees a LAZILY-allocated wait handle — allocated solely when
2309+ // `.AvailableWaitHandle` is read — so a SemaphoreSlim field used purely for Wait/WaitAsync/Release
2310+ // leaks nothing and is dispose-optional. GATE: if `.AvailableWaitHandle` IS read on the field,
2311+ // that handle exists and Dispose must release it, so the field STAYS tracked (Codex). Collect the
2312+ // field names whose AvailableWaitHandle is read (this/bare receiver) so the loop below keeps them.
2313+ // Scoped to FIELDS only — the shared IsDisposeOptional (and the flow-locals detector / the
2314+ // deliberate method-bounded `semLeak` control) is intentionally left untouched (CodeRabbit).
2315+ var waitHandleSemaphores = new HashSet < string > ( StringComparer . Ordinal ) ;
2316+ foreach ( var ma in cls . DescendantNodes ( ) . OfType < MemberAccessExpressionSyntax > ( ) )
2317+ if ( ma . Name . Identifier . Text == "AvailableWaitHandle" )
2318+ {
2319+ // credit the FIELD whose AvailableWaitHandle is read, by SYMBOL: a this/bare access bound
2320+ // to a real field symbol (so `other._f` and a shadowing local/param are NOT conflated —
2321+ // CodeRabbit), OR a field-ALIAS local (`var s = _sem; s.AvailableWaitHandle` -> `_sem` —
2322+ // Codex). Anything else (an unrelated local, another instance's field) is ignored.
2323+ var recv = model . GetSymbolInfo ( ma . Expression ) . Symbol ;
2324+ if ( recv is IFieldSymbol && ThisFieldName ( ma . Expression ) is { } whf )
2325+ waitHandleSemaphores . Add ( whf ) ;
2326+ else if ( recv is ILocalSymbol ls && aliasToField . TryGetValue ( ls , out var fa ) )
2327+ waitHandleSemaphores . Add ( fa ) ;
2328+ }
2329+
23072330 foreach ( var fd in cls . Members . OfType < FieldDeclarationSyntax > ( ) )
23082331 {
23092332 // a `static` IDisposable field is a process-lifetime singleton (a shared
@@ -2331,6 +2354,13 @@ or ImplicitObjectCreationExpressionSyntax
23312354 if ( eventSourceCounters . Contains ( v . Identifier . Text )
23322355 && DerivesFromDiagnosticCounter ( model . GetTypeInfo ( fd . Declaration . Type ) . Type ) )
23332356 continue ;
2357+ // a SemaphoreSlim field whose `.AvailableWaitHandle` is never read leaks nothing —
2358+ // Dispose() only frees that lazy handle — so it is dispose-optional and silent; if the
2359+ // handle IS read (in waitHandleSemaphores) it stays tracked. FIELD-scoped (Npgsql).
2360+ if ( ! waitHandleSemaphores . Contains ( v . Identifier . Text )
2361+ && model . GetTypeInfo ( fd . Declaration . Type ) . Type is { Name : "SemaphoreSlim" } st
2362+ && st . ContainingNamespace ? . ToString ( ) == "System.Threading" )
2363+ continue ;
23342364 subs . Add ( new
23352365 {
23362366 @event = v . Identifier . Text ,
0 commit comments