From d2f95994e615ea16e3ced6278ec4db0b29d0656c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 07:02:03 +0000 Subject: [PATCH 1/2] fix(extractor): SemaphoreSlim is dispose-optional + `.Close()` releases a field (Npgsql re-mine) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two precision fixes for the field-disposable detector, both surfaced by re-mining npgsql/npgsql after #88-#90 (which confirmed those three FP classes were cleared): #2 SemaphoreSlim dispose-optional. SemaphoreSlim.Dispose() only frees a LAZILY- allocated wait handle (allocated solely if AvailableWaitHandle is read); the common WaitAsync/Release usage allocates nothing, so an undisposed SemaphoreSlim field is not a leak. Added to IsDisposeOptional alongside Task/DataTable, which share the "Dispose is a no-op / lazy wait handle" rationale. Mined: NpgsqlDataSource._setupMappingsSemaphore. #1 `.Close()` releases a field. The field-disposable `disposed` scan credited a field only on Dispose()/DisposeAsync(), but the LOCAL detector (DisposesLocal and the flow detector) has always accepted Dispose/Close/DisposeAsync. That asymmetry flagged a field cleaned up by Close() as a leak. Mirror the local set — add Close to both the direct and null-conditional field-dispose checks — so a Stream / DbConnection-style field released by Close() is silent. Mined: Npgsql's ReplicationConnection releases its NpgsqlConnection via `await _npgsqlConnection.Close(async: true)` (a real Own.NET false positive). Regression sample SemaphoreAndCloseSample.cs: HoldsSemaphore._gate (SemaphoreSlim) silent; ReleasesViaClose._closedConn / _closedConnQ (direct + null-conditional Close) silent; controls still warn — HoldsRealDisposable._ctsControl (a non-optional CTS) and LeaksUnclosed._leakedConn (a connection-like field never closed). CI asserts all. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED --- .github/workflows/ci.yml | 18 ++++++ frontend/roslyn/OwnSharp.Extractor/Program.cs | 18 ++++-- .../roslyn/samples/SemaphoreAndCloseSample.cs | 64 +++++++++++++++++++ 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 frontend/roslyn/samples/SemaphoreAndCloseSample.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6361baa..da3726ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,6 +145,7 @@ jobs: frontend/roslyn/samples/EventSourceCountersSample.cs \ frontend/roslyn/samples/AppDomainShutdownSample.cs \ frontend/roslyn/samples/AliasDisposeSample.cs \ + frontend/roslyn/samples/SemaphoreAndCloseSample.cs \ -o "$RUNNER_TEMP/facts.json" cat "$RUNNER_TEMP/facts.json" - name: Check facts through the core @@ -261,6 +262,23 @@ jobs: # same-named local disposed in another method must NOT credit the field, so it still leaks. echo "$out" | grep -qE "AliasDisposeSample\.cs:[0-9]+:.*\[OWN001\].*'_scopedLeak'" \ || { echo "FAIL: a same-named local in another scope must not be miscredited (symbol-scoped aliases)"; exit 1; } + # #2 SemaphoreSlim is dispose-optional (Dispose only frees a lazy wait handle) -> an undisposed + # SemaphoreSlim field must be SILENT (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore). + if echo "$out" | grep -q "'_gate'"; then + echo "FAIL: an undisposed SemaphoreSlim field was wrongly reported (it is dispose-optional)"; exit 1 + fi + # #2 control: the exemption is SemaphoreSlim-specific — a real owned IDisposable (CTS) never + # disposed must STILL warn, proving it is not a blanket "any field" suppression. + echo "$out" | grep -qE "SemaphoreAndCloseSample\.cs:[0-9]+:.*\[OWN001\].*'_ctsControl'" \ + || { echo "FAIL: a non-optional owned IDisposable field must still warn (dispose-optional stays SemaphoreSlim-scoped)"; exit 1; } + # #1 a field released via `.Close()` (direct and null-conditional) must be SILENT — mirrors the + # local detector's Dispose/Close/DisposeAsync set (mined: Npgsql ReplicationConnection._npgsqlConnection). + if echo "$out" | grep -qE "'_closedConn'|'_closedConnQ'"; then + echo "FAIL: a field released via .Close() was wrongly reported as undisposed"; exit 1 + fi + # #1 control: a connection-like field NEITHER closed NOR disposed must STILL warn. + echo "$out" | grep -qE "SemaphoreAndCloseSample\.cs:[0-9]+:.*\[OWN001\].*'_leakedConn'" \ + || { echo "FAIL: a field that is never closed/disposed must still warn (Close-as-release stays scoped to an actual Close call)"; exit 1; } # WPF004: an ignored `X.Subscribe(...)` result leaks; the captured+ # disposed one stays silent. "ignored" is unique to the WPF004 message. echo "$out" | grep -q "MessengerViewModel.cs" \ diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index 3f37f7e0..c949b91c 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -469,7 +469,11 @@ static bool IsDisposeOptional(ITypeSymbol t) { var ns = t.ContainingNamespace?.ToString(); return (ns == "System.Threading.Tasks" && t.Name is "Task" or "ValueTask") - || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView"); + || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView") + // SemaphoreSlim.Dispose() only frees a LAZILY-allocated wait handle (allocated solely if + // AvailableWaitHandle is read); the common WaitAsync/Release usage allocates nothing, so an + // undisposed SemaphoreSlim field is not a leak. Mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore. + || (ns == "System.Threading" && t.Name == "SemaphoreSlim"); } // A type that is System.Windows.Forms.Form or derives from it (semantic, walks the @@ -2260,11 +2264,15 @@ or ImplicitObjectCreationExpressionSyntax && model.GetDeclaredSymbol(decl) is ILocalSymbol aliasSym && !reassignedAliases.Contains(aliasSym)) aliasToField[aliasSym] = af; - // a `.Dispose()`/`.DisposeAsync()` on a field — directly (`_f.Dispose()`) or through an - // alias local (translated by SYMBOL via aliasToField) — releases that field. + // a `.Dispose()`/`.DisposeAsync()`/`.Close()` on a field — directly (`_f.Dispose()`) or through + // an alias local (translated by SYMBOL via aliasToField) — releases that field. `Close()` counts + // as a release here exactly as it already does for LOCAL disposables (DisposesLocal and the flow + // detector both accept Dispose/Close/DisposeAsync); a field of a Stream / DbConnection-style type + // is released by Close just as Dispose would. Mined: Npgsql ReplicationConnection disposes its + // NpgsqlConnection field via `await _npgsqlConnection.Close(async: true)`. foreach (var inv in cls.DescendantNodes().OfType()) if (inv.Expression is MemberAccessExpressionSyntax m - && m.Name.Identifier.Text is "Dispose" or "DisposeAsync" + && m.Name.Identifier.Text is "Dispose" or "DisposeAsync" or "Close" && FieldName(m.Expression) is { } df) disposed.Add(model.GetSymbolInfo(m.Expression).Symbol is ILocalSymbol ls && aliasToField.TryGetValue(ls, out var fa) ? fa : df); @@ -2277,7 +2285,7 @@ or ImplicitObjectCreationExpressionSyntax foreach (var cae in cls.DescendantNodes().OfType()) if (FieldName(cae.Expression) is { } cdf && cae.WhenNotNull is InvocationExpressionSyntax { Expression: MemberBindingExpressionSyntax mb } - && mb.Name.Identifier.Text is "Dispose" or "DisposeAsync") + && mb.Name.Identifier.Text is "Dispose" or "DisposeAsync" or "Close") disposed.Add(model.GetSymbolInfo(cae.Expression).Symbol is ILocalSymbol lc && aliasToField.TryGetValue(lc, out var fc) ? fc : cdf); diff --git a/frontend/roslyn/samples/SemaphoreAndCloseSample.cs b/frontend/roslyn/samples/SemaphoreAndCloseSample.cs new file mode 100644 index 00000000..592ec6c5 --- /dev/null +++ b/frontend/roslyn/samples/SemaphoreAndCloseSample.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading; + +namespace Own.Samples; + +// Two Npgsql re-mine precision fixes for the field-disposable (WPF003/OWN001) detector. +// +// #2 SemaphoreSlim is dispose-optional: its Dispose() only frees a LAZILY-allocated wait handle +// (allocated solely if AvailableWaitHandle is read), so the common WaitAsync/Release usage +// leaks nothing and an undisposed SemaphoreSlim field must be SILENT. Mined: +// NpgsqlDataSource._setupMappingsSemaphore. +// #1 `.Close()` releases a field, exactly as it already does for LOCAL disposables (DisposesLocal +// and the flow detector both accept Dispose/Close/DisposeAsync): a Stream / DbConnection-style +// field cleaned up by Close() is not a leak. Mined: ReplicationConnection releases its +// NpgsqlConnection via `await _npgsqlConnection.Close(async: true)`. + +// #2: a SemaphoreSlim field new'd and never disposed -> dispose-optional -> SILENT. +public sealed class HoldsSemaphore +{ + private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); + + public void Use() + { + _gate.Wait(); + _gate.Release(); + } +} + +// #2 control: the exemption is SemaphoreSlim-specific, NOT "any field" — a real owned IDisposable +// (CancellationTokenSource) new'd and never disposed must STILL warn OWN001. +public sealed class HoldsRealDisposable +{ + private readonly CancellationTokenSource _ctsControl = new CancellationTokenSource(); + + public void Cancel() => _ctsControl.Cancel(); +} + +// #1: a field released via `.Close()` — direct and null-conditional — must be SILENT. +public sealed class ReleasesViaClose : IDisposable +{ + private readonly FakeConnection _closedConn = new FakeConnection(); + private readonly FakeConnection _closedConnQ = new FakeConnection(); + + public void Dispose() + { + _closedConn.Close(); // Close() releases the field -> SILENT + _closedConnQ?.Close(); // null-conditional Close() -> SILENT + } +} + +// #1 control: a field NEITHER closed NOR disposed must STILL warn OWN001. +public sealed class LeaksUnclosed : IDisposable +{ + private readonly FakeConnection _leakedConn = new FakeConnection(); + + public void Dispose() { } // never closed/disposed -> WARN +} + +// A connection-like resource whose release is Close() (the DbConnection.Close() / Stream.Close() shape). +internal sealed class FakeConnection : IDisposable +{ + public void Close() { } + public void Dispose() { } +} From 3648802410560b123701b258848a659754d4b1a2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Jun 2026 07:12:25 +0000 Subject: [PATCH 2/2] fix(extractor): recognize `.Close()` as a field release (receiver-scoped); drop the SemaphoreSlim exemption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Scopes this PR to the uncontentious fix and addresses the review. #1 `.Close()` releases a field — the field-disposable `disposed` scan credited a field only on Dispose()/DisposeAsync(), but the LOCAL detector (DisposesLocal and the flow detector) has always accepted Dispose/Close/DisposeAsync. That asymmetry flagged a field cleaned up by Close() as a leak. Mirror the local set on both the direct and null-conditional field-release checks. Mined: Npgsql ReplicationConnection releases its NpgsqlConnection field via `await _npgsqlConnection.Close(async: true)`. Receiver-scoped (Codex/CodeRabbit): credit only THIS instance's field or a validated alias via ThisFieldName, so `other._conn.Close()` on ANOTHER instance of the SAME class cannot mark this object's `_conn` released. (A field-symbol ContainingType check would NOT catch this — same class, same ContainingType — so the receiver is keyed syntactically; this also tightens the pre-existing Dispose path.) Dropped: the SemaphoreSlim dispose-optional change. Putting SemaphoreSlim in the shared IsDisposeOptional also affected the flow-locals detector and tripped the deliberate `semLeak` control (a prior ShareX fix explicitly decided SemaphoreSlim stays tracked for method-bounded locals, since reading AvailableWaitHandle allocates a handle Dispose must release — Codex flagged the same). A sound, field-scoped + AvailableWaitHandle-gated version is a separate decision, deferred. Regression sample CloseReleaseSample.cs: ReleasesViaClose._closedConn / _closedConnQ (direct + null-conditional Close) silent; controls still warn — LeaksUnclosed._leakedConn (never closed) and ClosesOtherInstanceField._xconn (closed only on ANOTHER instance). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED --- .github/workflows/ci.yml | 22 +++---- frontend/roslyn/OwnSharp.Extractor/Program.cs | 26 ++++---- frontend/roslyn/samples/CloseReleaseSample.cs | 52 +++++++++++++++ .../roslyn/samples/SemaphoreAndCloseSample.cs | 64 ------------------- 4 files changed, 74 insertions(+), 90 deletions(-) create mode 100644 frontend/roslyn/samples/CloseReleaseSample.cs delete mode 100644 frontend/roslyn/samples/SemaphoreAndCloseSample.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da3726ca..2b62d4bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,7 +145,7 @@ jobs: frontend/roslyn/samples/EventSourceCountersSample.cs \ frontend/roslyn/samples/AppDomainShutdownSample.cs \ frontend/roslyn/samples/AliasDisposeSample.cs \ - frontend/roslyn/samples/SemaphoreAndCloseSample.cs \ + frontend/roslyn/samples/CloseReleaseSample.cs \ -o "$RUNNER_TEMP/facts.json" cat "$RUNNER_TEMP/facts.json" - name: Check facts through the core @@ -262,23 +262,19 @@ jobs: # same-named local disposed in another method must NOT credit the field, so it still leaks. echo "$out" | grep -qE "AliasDisposeSample\.cs:[0-9]+:.*\[OWN001\].*'_scopedLeak'" \ || { echo "FAIL: a same-named local in another scope must not be miscredited (symbol-scoped aliases)"; exit 1; } - # #2 SemaphoreSlim is dispose-optional (Dispose only frees a lazy wait handle) -> an undisposed - # SemaphoreSlim field must be SILENT (mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore). - if echo "$out" | grep -q "'_gate'"; then - echo "FAIL: an undisposed SemaphoreSlim field was wrongly reported (it is dispose-optional)"; exit 1 - fi - # #2 control: the exemption is SemaphoreSlim-specific — a real owned IDisposable (CTS) never - # disposed must STILL warn, proving it is not a blanket "any field" suppression. - echo "$out" | grep -qE "SemaphoreAndCloseSample\.cs:[0-9]+:.*\[OWN001\].*'_ctsControl'" \ - || { echo "FAIL: a non-optional owned IDisposable field must still warn (dispose-optional stays SemaphoreSlim-scoped)"; exit 1; } - # #1 a field released via `.Close()` (direct and null-conditional) must be SILENT — mirrors the + # a field released via `.Close()` (direct and null-conditional) must be SILENT — mirrors the # local detector's Dispose/Close/DisposeAsync set (mined: Npgsql ReplicationConnection._npgsqlConnection). if echo "$out" | grep -qE "'_closedConn'|'_closedConnQ'"; then echo "FAIL: a field released via .Close() was wrongly reported as undisposed"; exit 1 fi - # #1 control: a connection-like field NEITHER closed NOR disposed must STILL warn. - echo "$out" | grep -qE "SemaphoreAndCloseSample\.cs:[0-9]+:.*\[OWN001\].*'_leakedConn'" \ + # control: a connection-like field NEITHER closed NOR disposed must STILL warn. + echo "$out" | grep -qE "CloseReleaseSample\.cs:[0-9]+:.*\[OWN001\].*'_leakedConn'" \ || { echo "FAIL: a field that is never closed/disposed must still warn (Close-as-release stays scoped to an actual Close call)"; exit 1; } + # Codex/CodeRabbit control: Close() credits THIS instance's field only — closing ANOTHER instance + # of the same class's same-named field must NOT suppress this object's leak (ThisFieldName, not a + # receiver-stripping name match that a same-class ContainingType check would also miss). + echo "$out" | grep -qE "CloseReleaseSample\.cs:[0-9]+:.*\[OWN001\].*'_xconn'" \ + || { echo "FAIL: other-instance .Close() must not credit this field (receiver-scoped to this/alias)"; exit 1; } # WPF004: an ignored `X.Subscribe(...)` result leaks; the captured+ # disposed one stays silent. "ignored" is unique to the WPF004 message. echo "$out" | grep -q "MessengerViewModel.cs" \ diff --git a/frontend/roslyn/OwnSharp.Extractor/Program.cs b/frontend/roslyn/OwnSharp.Extractor/Program.cs index c949b91c..bd17fd9b 100644 --- a/frontend/roslyn/OwnSharp.Extractor/Program.cs +++ b/frontend/roslyn/OwnSharp.Extractor/Program.cs @@ -469,11 +469,7 @@ static bool IsDisposeOptional(ITypeSymbol t) { var ns = t.ContainingNamespace?.ToString(); return (ns == "System.Threading.Tasks" && t.Name is "Task" or "ValueTask") - || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView") - // SemaphoreSlim.Dispose() only frees a LAZILY-allocated wait handle (allocated solely if - // AvailableWaitHandle is read); the common WaitAsync/Release usage allocates nothing, so an - // undisposed SemaphoreSlim field is not a leak. Mined: Npgsql NpgsqlDataSource._setupMappingsSemaphore. - || (ns == "System.Threading" && t.Name == "SemaphoreSlim"); + || (ns == "System.Data" && t.Name is "DataTable" or "DataSet" or "DataView"); } // A type that is System.Windows.Forms.Form or derives from it (semantic, walks the @@ -2264,16 +2260,20 @@ or ImplicitObjectCreationExpressionSyntax && model.GetDeclaredSymbol(decl) is ILocalSymbol aliasSym && !reassignedAliases.Contains(aliasSym)) aliasToField[aliasSym] = af; - // a `.Dispose()`/`.DisposeAsync()`/`.Close()` on a field — directly (`_f.Dispose()`) or through - // an alias local (translated by SYMBOL via aliasToField) — releases that field. `Close()` counts - // as a release here exactly as it already does for LOCAL disposables (DisposesLocal and the flow - // detector both accept Dispose/Close/DisposeAsync); a field of a Stream / DbConnection-style type - // is released by Close just as Dispose would. Mined: Npgsql ReplicationConnection disposes its - // NpgsqlConnection field via `await _npgsqlConnection.Close(async: true)`. + // a `.Dispose()`/`.DisposeAsync()`/`.Close()` on a field — directly (`_f.Dispose()` / `this._f.…`) + // or through an alias local (translated by SYMBOL via aliasToField) — releases that field. + // `Close()` counts as a release here exactly as it already does for LOCAL disposables (DisposesLocal + // and the flow detector both accept Dispose/Close/DisposeAsync); a Stream / DbConnection-style field + // released by Close is not a leak. Mined: Npgsql ReplicationConnection disposes its NpgsqlConnection + // field via `await _npgsqlConnection.Close(async: true)`. ThisFieldName (not FieldName) scopes the + // credit to THIS instance's field / a validated alias: `other._f.Close()` on ANOTHER instance of + // the same class must NOT mark this object's `_f` released (Codex/CodeRabbit) — and a field-symbol + // ContainingType check would NOT catch that (same class -> same ContainingType), so key on the + // `this`/bare receiver syntactically. foreach (var inv in cls.DescendantNodes().OfType()) if (inv.Expression is MemberAccessExpressionSyntax m && m.Name.Identifier.Text is "Dispose" or "DisposeAsync" or "Close" - && FieldName(m.Expression) is { } df) + && ThisFieldName(m.Expression) is { } df) disposed.Add(model.GetSymbolInfo(m.Expression).Symbol is ILocalSymbol ls && aliasToField.TryGetValue(ls, out var fa) ? fa : df); // Also the NULL-CONDITIONAL form `field?.Dispose()` (a ConditionalAccess whose @@ -2283,7 +2283,7 @@ or ImplicitObjectCreationExpressionSyntax // and the BufferedStreams benchmark's `[GlobalCleanup]` `field?.Dispose()` calls. // (The same alias-by-symbol translation applies — `cts?.Dispose()` on an aliasing local.) foreach (var cae in cls.DescendantNodes().OfType()) - if (FieldName(cae.Expression) is { } cdf + if (ThisFieldName(cae.Expression) is { } cdf // this-instance field / alias only (not `other._f?.Close()`) && cae.WhenNotNull is InvocationExpressionSyntax { Expression: MemberBindingExpressionSyntax mb } && mb.Name.Identifier.Text is "Dispose" or "DisposeAsync" or "Close") disposed.Add(model.GetSymbolInfo(cae.Expression).Symbol is ILocalSymbol lc diff --git a/frontend/roslyn/samples/CloseReleaseSample.cs b/frontend/roslyn/samples/CloseReleaseSample.cs new file mode 100644 index 00000000..c5ac84d0 --- /dev/null +++ b/frontend/roslyn/samples/CloseReleaseSample.cs @@ -0,0 +1,52 @@ +using System; + +namespace Own.Samples; + +// `.Close()` releases a field, exactly as it already does for LOCAL disposables (DisposesLocal and the +// flow detector both accept Dispose/Close/DisposeAsync): a Stream / DbConnection-style field cleaned up +// by Close() is not a leak. Mined: Npgsql's ReplicationConnection releases its NpgsqlConnection field +// via `await _npgsqlConnection.Close(async: true)`. + +// a field released via `.Close()` — direct and null-conditional — must be SILENT. +public sealed class ReleasesViaClose : IDisposable +{ + private readonly FakeConnection _closedConn = new FakeConnection(); + private readonly FakeConnection _closedConnQ = new FakeConnection(); + + public void Dispose() + { + _closedConn.Close(); // Close() releases the field -> SILENT + _closedConnQ?.Close(); // null-conditional Close() -> SILENT + } +} + +// control: a field NEITHER closed NOR disposed must STILL warn OWN001. +public sealed class LeaksUnclosed : IDisposable +{ + private readonly FakeConnection _leakedConn = new FakeConnection(); + + public void Dispose() { } // never closed/disposed -> WARN +} + +// Codex/CodeRabbit control: Close() must target THIS instance's field. Closing ANOTHER instance of the +// SAME class's same-named private field must NOT credit this object — and note a field-symbol +// ContainingType check could not tell them apart (same class), so this leans on the `this`/bare receiver. +// This object's own _xconn is never closed, so it STILL leaks. +public sealed class ClosesOtherInstanceField : IDisposable +{ + private readonly FakeConnection _xconn = new FakeConnection(); + + public void CloseOther(ClosesOtherInstanceField other) + { + other._xconn.Close(); // closes ANOTHER instance's field -> must NOT credit this._xconn + } + + public void Dispose() { } // this._xconn is never closed -> WARN OWN001 +} + +// A connection-like resource whose release is Close() (the DbConnection.Close() / Stream.Close() shape). +internal sealed class FakeConnection : IDisposable +{ + public void Close() { } + public void Dispose() { } +} diff --git a/frontend/roslyn/samples/SemaphoreAndCloseSample.cs b/frontend/roslyn/samples/SemaphoreAndCloseSample.cs deleted file mode 100644 index 592ec6c5..00000000 --- a/frontend/roslyn/samples/SemaphoreAndCloseSample.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Threading; - -namespace Own.Samples; - -// Two Npgsql re-mine precision fixes for the field-disposable (WPF003/OWN001) detector. -// -// #2 SemaphoreSlim is dispose-optional: its Dispose() only frees a LAZILY-allocated wait handle -// (allocated solely if AvailableWaitHandle is read), so the common WaitAsync/Release usage -// leaks nothing and an undisposed SemaphoreSlim field must be SILENT. Mined: -// NpgsqlDataSource._setupMappingsSemaphore. -// #1 `.Close()` releases a field, exactly as it already does for LOCAL disposables (DisposesLocal -// and the flow detector both accept Dispose/Close/DisposeAsync): a Stream / DbConnection-style -// field cleaned up by Close() is not a leak. Mined: ReplicationConnection releases its -// NpgsqlConnection via `await _npgsqlConnection.Close(async: true)`. - -// #2: a SemaphoreSlim field new'd and never disposed -> dispose-optional -> SILENT. -public sealed class HoldsSemaphore -{ - private readonly SemaphoreSlim _gate = new SemaphoreSlim(1, 1); - - public void Use() - { - _gate.Wait(); - _gate.Release(); - } -} - -// #2 control: the exemption is SemaphoreSlim-specific, NOT "any field" — a real owned IDisposable -// (CancellationTokenSource) new'd and never disposed must STILL warn OWN001. -public sealed class HoldsRealDisposable -{ - private readonly CancellationTokenSource _ctsControl = new CancellationTokenSource(); - - public void Cancel() => _ctsControl.Cancel(); -} - -// #1: a field released via `.Close()` — direct and null-conditional — must be SILENT. -public sealed class ReleasesViaClose : IDisposable -{ - private readonly FakeConnection _closedConn = new FakeConnection(); - private readonly FakeConnection _closedConnQ = new FakeConnection(); - - public void Dispose() - { - _closedConn.Close(); // Close() releases the field -> SILENT - _closedConnQ?.Close(); // null-conditional Close() -> SILENT - } -} - -// #1 control: a field NEITHER closed NOR disposed must STILL warn OWN001. -public sealed class LeaksUnclosed : IDisposable -{ - private readonly FakeConnection _leakedConn = new FakeConnection(); - - public void Dispose() { } // never closed/disposed -> WARN -} - -// A connection-like resource whose release is Close() (the DbConnection.Close() / Stream.Close() shape). -internal sealed class FakeConnection : IDisposable -{ - public void Close() { } - public void Dispose() { } -}