Skip to content

fix(extractor): recognize a field disposed through a local alias (mined Npgsql NpgsqlDataSource)#90

Merged
PhysShell merged 2 commits into
mainfrom
claude/fp-alias-dispose
Jun 23, 2026
Merged

fix(extractor): recognize a field disposed through a local alias (mined Npgsql NpgsqlDataSource)#90
PhysShell merged 2 commits into
mainfrom
claude/fp-alias-dispose

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 23, 2026

Copy link
Copy Markdown
Owner

What

The field-disposable detector credited a field as released only on a direct _field.Dispose() / _field?.Dispose(). Npgsql's NpgsqlDataSource disposes its CancellationTokenSource through a local copy:

public void Dispose()
{
    var cts = _cts;
    cts.Dispose();   // _cts IS released — but the detector only saw `cts.Dispose()`
}

So the field looked undisposed and was reported as an OWN001 leak — a false positive.

How (sound, three gates)

Teach the disposed scan to follow a local that aliases a field: map each var a = _f; / var a = this._f; → its field, then translate a .Dispose() / ?.Dispose() on the alias to the underlying field. The alias is recognized only when:

  1. the initializer is a this/bare field reference (never other._f);
  2. it binds to a real field symbol (not a same-named local); and
  3. the alias is never reassigned anywhere — a rebound local no longer tracks the field, so we conservatively decline to credit it (the finding stays).

Direct disposals are unchanged: a field with no alias entry passes through the translation untouched, so this is purely additive.

Mined

Npgsql.NpgsqlDataSource disposes _cts via var cts = _cts; cts.Dispose(); — the last documented FP class from the Npgsql mine.

Regression sample (AliasDisposeSample.cs)

  • AliasDisposes_aliased (bare alias + .Dispose()) and _aliasedQ (this._f alias + ?.Dispose()) → SILENT.
  • AliasNeverDisposes_neverDisposed, aliased but never disposed → STILL warns (recognition needs an actual dispose).
  • ReboundAliasLeaks_rebound, alias rebound to a new source before disposal → STILL warns (reassignment gate).

CI asserts all three. The corpus benchmark guards against any real-C# recall regression.

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • Improvements
    • Enhanced disposable-field release detection to treat Dispose/DisposeAsync called on eligible local aliases (including null-conditional calls) as releases of the underlying fields, improving results for undisposed-field warnings.
    • Improved tracking to avoid incorrect attribution when aliases are rebound via ref/out helpers or when multiple locals share the same alias name across different scopes.
  • Tests / CI
    • Updated validation coverage to include the expanded alias disposal scenarios in the WPF sample, including new edge cases.

…ed Npgsql NpgsqlDataSource)

The field-disposable detector credited a field as released only on a DIRECT
`_field.Dispose()` / `_field?.Dispose()`. Npgsql's NpgsqlDataSource disposes its
CancellationTokenSource through a local copy — `var cts = _cts; cts.Dispose();` —
so the field looked undisposed and was reported as an OWN001 leak (false positive).

Teach the `disposed` scan to follow a local that aliases a field: map each
`var a = _f;` / `var a = this._f;` to its field, then translate a `.Dispose()` /
`?.Dispose()` on the alias to the underlying field. Three gates keep it sound:

- the alias initializer is a `this`/bare field reference (never `other._f`);
- it BINDS to a real field symbol (not a same-named local); and
- the alias is never REASSIGNED anywhere — a rebound local no longer tracks the
  field, so we conservatively decline to credit it (keeps the finding).

Direct disposals are unchanged: a field with no alias entry passes through the
translation untouched.

Regression sample AliasDisposeSample.cs: AliasDisposes (`_aliased` via bare alias,
`_aliasedQ` via `this._f` + `?.Dispose()`) is SILENT; the controls still warn —
AliasNeverDisposes (`_neverDisposed`, aliased but never disposed) and
ReboundAliasLeaks (`_rebound`, alias rebound to a new source). CI asserts all three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
// recognition requires an actual `.Dispose()` on the alias, not merely a copy.
public sealed class AliasNeverDisposes : IDisposable
{
private readonly CancellationTokenSource _neverDisposed = new CancellationTokenSource();
// not the field, so the field must STILL leak (the reassignment gate declines to credit it).
public sealed class ReboundAliasLeaks : IDisposable
{
private readonly CancellationTokenSource _rebound = new CancellationTokenSource();
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c1626394-97ed-4510-9869-5fc58dde8f39

📥 Commits

Reviewing files that changed from the base of the PR and between c678ab6 and 5a3ba52.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/AliasDisposeSample.cs
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs

📝 Walkthrough

Walkthrough

The WPF003 disposable-field scanner in OwnSharp.Extractor is extended to recognize disposal through local aliases of class fields. Five sample classes covering alias disposal, never-disposed aliases, rebound aliases, ref-rebound aliases, and scoped symbol aliasing are added. CI assertions validate that correctly aliased disposals are silent while leaking variants still emit OWN001.

Changes

Alias Disposal Detection

Layer / File(s) Summary
Sample classes for alias disposal scenarios
frontend/roslyn/samples/AliasDisposeSample.cs
Adds five classes: AliasDisposes, AliasNeverDisposes, ReboundAliasLeaks, RefReboundAliasLeaks, and ScopedAliasNameLeak, covering alias disposal patterns, reassignment, ref-rebound, and symbol-scoped alias handling.
WPF003 alias-to-field disposal tracking
frontend/roslyn/OwnSharp.Extractor/Program.cs
Builds an alias-to-field map from non-reassigned locals (excluding ref/out parameters), then translates Dispose/DisposeAsync and null-conditional dispose calls on alias locals into disposal credits against the underlying field.
CI registration and OWN001 assertions
.github/workflows/ci.yml
Registers AliasDisposeSample.cs in the extractor sample list and adds grep-based CI assertions: no OWN001 for correctly aliased fields, OWN001 required for never-disposed, rebound, ref-rebound, and scoped-leak fields.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PhysShell/Own.NET#86: Extends WPF003 disposable-field release detection for null-conditional field?.Dispose() patterns in the same extractor file, which this PR also touches and extends with alias translation.

Poem

🐇 A field gets an alias, a local with flair,
The scanner now follows it everywhere with care!
Rebound or ref-passed? The alias breaks free—
OWN001 rings out: "That field's the debris!"
But disposed via alias? Silent and clean.
✨ The extractor hops to the finest Dispose it's seen!

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: fixing the extractor to recognize field disposal through local aliases, with the specific Npgsql NpgsqlDataSource case mentioned.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fp-alias-dispose

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c678ab65c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2211 to +2213
foreach (var a in assigns)
if (a.Left is IdentifierNameSyntax lid)
reassignedLocals.Add(lid.Identifier.Text);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat ref/out alias arguments as reassignments

This reassignment gate only looks at assignment expressions, so it still credits the field when the alias is rebound through ref/out before disposal, e.g. var a = _cts; Reset(out a); a.Dispose();. In that scenario the helper can replace a with a new object, the original field leaks, and the extractor now suppresses the OWN001 finding because the alias name was never added to reassignedLocals.

Useful? React with 👍 / 👎.

&& ThisFieldName(init) is { } af
&& !reassignedLocals.Contains(decl.Identifier.Text)
&& model.GetSymbolInfo(init).Symbol is IFieldSymbol)
aliasToField[decl.Identifier.Text] = af;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope alias translations to the actual local symbol

Keying aliases only by identifier for the whole class makes any same-named dispose translate to this field, even outside the alias's scope. For example, if one method contains var cts = _cts; but another method disposes an unrelated local or member named cts, cts.Dispose() is credited as releasing _cts, hiding a real undisposed-field leak; similarly, two methods using the same alias name for different fields overwrite each other in this dictionary.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/roslyn/OwnSharp.Extractor/Program.cs`:
- Around line 2210-2236: The aliasToField dictionary uses class-wide plain name
strings as keys, causing same-named locals across different scopes to collide
and allowing non-local declarators to be incorrectly included in the mapping.
Refactor the alias tracking to use symbol-based identification instead of plain
strings by examining the ISymbol returned from model.GetSymbolInfo(init) and
only including declarators that represent local variables (check symbol kind or
verify the parent scope context). Then update the disposal detection logic in
both the InvocationExpressionSyntax and ConditionalAccessExpressionSyntax loops
to perform symbol-based alias lookups instead of string-based dictionary lookups
against aliasToField, ensuring disposal credits are correctly attributed to the
right field within the correct scope.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6c156178-2f45-4bb2-b23d-e4c6119a4e6e

📥 Commits

Reviewing files that changed from the base of the PR and between 19d35d9 and c678ab6.

📒 Files selected for processing (3)
  • .github/workflows/ci.yml
  • frontend/roslyn/OwnSharp.Extractor/Program.cs
  • frontend/roslyn/samples/AliasDisposeSample.cs

Comment thread frontend/roslyn/OwnSharp.Extractor/Program.cs Outdated
…t rebinds (Codex P2 + CodeRabbit)

The first cut keyed the alias map by identifier STRING, scanned class-wide, and
gated reassignment only on `=` expressions. Three holes, all flagged in review:

- CodeRabbit (major) + Codex #2 — class-wide name keys conflate same-named locals
  across methods: an unrelated `c.Dispose()` in one method could credit a field
  aliased by a `c` in another (FN), and two aliases reusing a name overwrite each
  other. Now keyed by the LOCAL SYMBOL (model.GetDeclaredSymbol(decl)), and the
  dispose scan resolves the receiver symbol — so each alias is scope-exact.
- Codex #1 — an alias rebound through a `ref`/`out` argument (`Swap(ref a)`) was
  not counted as reassigned, so a disposed-after-rebind alias wrongly credited the
  field (FN). The reassignment set now includes ref/out argument locals.
- Iterating declarators by symbol (is ILocalSymbol) also drops non-local
  declarators from the map (locals-only, as intended).

Direct field disposals are unchanged: a receiver that is not an alias local falls
back to FieldName, exactly as before.

Regression controls added: RefReboundAliasLeaks (`Swap(ref a)` then dispose ->
_refRebound still leaks) and ScopedAliasNameLeak (an unrelated same-named `c`
disposed in another method must not credit _scopedLeak -> it still leaks). CI
asserts both, alongside the existing aliased-silent and never-disposed/`=`-rebound
controls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
// assignment expressions.
public sealed class RefReboundAliasLeaks : IDisposable
{
private readonly CancellationTokenSource _refRebound = new CancellationTokenSource();
// the leak; symbol scoping keeps them distinct, so the field STILL leaks (WARN OWN001).
public sealed class ScopedAliasNameLeak : IDisposable
{
private readonly CancellationTokenSource _scopedLeak = new CancellationTokenSource();
@PhysShell PhysShell merged commit f9605da into main Jun 23, 2026
22 checks passed
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
PhysShell pushed a commit that referenced this pull request Jun 23, 2026
…field + alias reads (Codex + CodeRabbit)

The first cut keyed the AvailableWaitHandle gate by bare identifier text via
ThisFieldName, which (a) missed a read through a field ALIAS — `var s = _sem;
s.AvailableWaitHandle` recorded `s`, not `_sem`, so the field stayed wrongly
exempt (Codex) — and (b) could conflate a shadowing local/parameter with a
same-named field (CodeRabbit).

Resolve the receiver by SYMBOL: credit the field's AvailableWaitHandle read only
when the receiver binds to a real field symbol via a this/bare access (excludes
`other._f` and shadowing locals), OR to a field-alias local (reusing the #90
aliasToField map). Anything else is ignored.

Regression: SemaphoreFieldSample gains AliasedWaitHandleSemaphore._aliasedSem
(AvailableWaitHandle read through `var s = _aliasedSem`) which must STILL warn —
the field is tracked through the alias. The existing controls (_optionalSem
silent, _handleSem direct-read warns, _ctsControl type-scope warns) are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants