Skip to content

feat(own): disposable-local → using, and inline-lambda extraction#4

Merged
PhysShell merged 3 commits into
mainfrom
claude/sts-runtime-analysis-2mo4z9
Jun 25, 2026
Merged

feat(own): disposable-local → using, and inline-lambda extraction#4
PhysShell merged 3 commits into
mainfrom
claude/sts-runtime-analysis-2mo4z9

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 25, 2026

Copy link
Copy Markdown
Owner

OWN fixer: disposable-local → using, and inline-lambda extraction

Adds the two hardest OWN001 shapes to the T4 fixer — the ones deliberately left suggest-only in PR #2 because they need structural understanding. Both are conservative by design (T4: refuse rather than emit a wrong patch). plan_file was generalized to an edit engine ((start, end, repl) applied bottom-up) so multi-line rewrites compose; brace/scope analysis is char-level and ignores strings + // comments.

3. disposable local → block using

- var myProcess = new Process();
+ using (var myProcess = new Process())
+ {
      myProcess.Start();
+ }

Fires only when the local doesn't escape its blockreturn/out/ref/store → refuse local-escapes (otherwise we'd dispose before last use).

4. inline-lambda subscription → extract + detach

- stage.PropertyChanged += (s2, e2) => OnPropertyChanged("Stages");
+ stage.PropertyChanged += OnStagePropertyChanged;
+ this.Closed += (s, e) => stage.PropertyChanged -= OnStagePropertyChanged;
+ private void OnStagePropertyChanged(object s2, PropertyChangedEventArgs e2) => OnPropertyChanged("Stages");

Only for well-known event delegates (PropertyChanged / ListChanged / CollectionChanged) with a 2-param expression lambda; block bodies, unknown delegates, multi-line subs → refuse.

Honesty boundary

Every refusal is surfaced in applier.skipped, never a fake patch: local-escapes, lambda-shape-unsupported, unknown-event-delegate, unbraced-control-flow, no-safe-teardown.

The OWN fixer now covers four shapes (subscription, field, local, lambda). Fixtures are real STS sites (Helper, DatabaseOptimizationWindow, plus a block-body refusal variant).

Test plan

PYTHONPATH=fix python3 fix/tests/test_own_fix.py        # 16/16 (incl. escaping-local + block-lambda refusals)
PYTHONPATH=fix python3 -O fix/tests/test_own_fix.py     # 16/16 under -O
PYTHONPATH=fix python3 fix/tests/test_orchestrate.py    # 7/7

🤖 Generated with Claude Code


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Expanded OWN fix suggestions to cover additional leak patterns: disposable fields disposed during teardown, disposable locals wrapped in scoped using, and inline event lambdas extracted into named handlers with teardown detachment.
  • Bug Fixes
    • Improved safety by refusing ambiguous or unsafe patterns and surfacing skip reasons instead of applying partial changes.
    • Better support for integrating fixes into existing teardown flows when available.
  • Documentation
    • Updated OWN fixer documentation to describe all supported shapes and refusal behavior.
  • Tests
    • Added/updated end-to-end and fixture coverage for supported and skipped scenarios.

claude added 2 commits June 25, 2026 16:09
Extends the OWN fixer (PR #2) to a second leak shape: an IDisposable field that is
never disposed (Timer, CancellationTokenSource, ...). On a WPF owner it disposes the
field on the teardown event, reusing the same Window->Closed / FrameworkElement->
Unloaded selection as the subscription detach:

    public ShareWindow() {
        InitializeComponent();
+       this.Closed += (s, e) => _timer?.Dispose();

The dispose hook is anchored after the ctor's InitializeComponent() (in scope for
'this'). Honesty boundary kept: disposable-LOCAL stays suggest-only (needs a scoped
using), as does the inline-lambda subscription. classify() now also returns the field
shape; plan_file dispatches per shape to the right anchor + teardown statement.

Fixture: ShareWindow._timer (real STS site). +2 tests; 13/13 own + 7/7 wrapper, incl -O.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa
Two new OWN001 shapes for the T4 fixer, both conservative (T4: refuse rather
than emit a wrong patch). Generalized plan_file to an edit engine
((start,end,repl) applied bottom-up) so multi-line rewrites compose.

- disposable LOCAL: wrap a clean 'T x = new T(...);' in a block 'using (...) {}',
  but only when the local doesn't escape its block (return/out/ref/store -> refuse
  'local-escapes', no use-after-dispose).
- inline-LAMBDA subscription: extract the lambda to a named handler, rewrite '+='
  to the method group, add the teardown detach. Only well-known event delegates
  (PropertyChanged/ListChanged/CollectionChanged) with a 2-param expression lambda;
  block bodies / unknown delegates / multi-line subs -> refuse.

Scope analysis is char-level brace matching that ignores strings and // comments.
Fixtures: Helper (local), DatabaseOptimizationWindow (lambda extract + a block-body
refusal variant). +4 tests incl. escaping-local and block-lambda refusals.
16/16 own + 7/7 wrapper, normal and -O.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3da33d51-8352-4463-b581-9730b36921c4

📥 Commits

Reviewing files that changed from the base of the PR and between 67574ae and 0088e83.

📒 Files selected for processing (2)
  • fix/fixarm/own_fix.py
  • fix/tests/test_own_fix.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • fix/fixarm/own_fix.py

📝 Walkthrough

Walkthrough

The PR expands the OWN001 fixer to classify named-handler, disposable-field, disposable-local, and inline-lambda findings, adds planning and application logic for those shapes, and updates docs, fixtures, and tests to match the new supported and skipped cases.

Changes

OWN001 fixer expansion

Layer / File(s) Summary
Shape contract and docs
docs/fix-arm.md, fix/README.md, fix/fixarm/own_fix.py
Docs and classifier text now describe four supported OWN001 shapes and explicit skip reporting for unsupported findings.
Teardown and extraction planners
fix/fixarm/own_fix.py
Constructor-anchor lookup, shared teardown placement, disposable-local wrapping, inline-lambda extraction, and multi-edit application are added.
Fixtures and rewrite coverage
fix/fixtures/own001-disposable-field/*, fix/fixtures/own001-disposable-local/*, fix/fixtures/own001-lambda/*, fix/fixtures/own001-lambda-extract/*, fix/tests/test_own_fix.py
Fixture inputs and end-to-end tests cover disposable-field, disposable-local, inline-lambda extraction, and unsupported escape or block-body cases.

Sequence Diagram(s)

sequenceDiagram
  participant plan_file
  participant classify
  participant _plan_one
  participant _plan_teardown
  participant _plan_local
  participant _plan_lambda
  plan_file ->> classify: inspect finding message
  classify -->> plan_file: shape category
  plan_file ->> _plan_one: dispatch finding
  _plan_one ->> _plan_teardown: named-handler / disposable-field
  _plan_one ->> _plan_local: disposable-local
  _plan_one ->> _plan_lambda: inline-lambda extract+detach
  _plan_teardown -->> plan_file: edits or skip detail
  _plan_local -->> plan_file: edits or skip detail
  _plan_lambda -->> plan_file: edits or skip detail
  plan_file ->> plan_file: apply edits in reverse index order
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PhysShell/Own.NET#59: Closely related local-escape detection and skip behavior for OWN001-shaped findings.
  • PhysShell/OwnAudit#2: Earlier OWN fixer pipeline work that this PR extends from single-shape handling to multi-shape planning and application.

Poem

🐰 I hopped through the OWN001 code at dawn,
and stitched new teardowns where leaks were drawn.
I wrapped the locals, named the lambdas, too,
then skipped the odd shapes that wouldn't safely do.
Hop-hop—clean patches in the burrow now bloom.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 matches the main change: disposable-local rewriting to using blocks and inline-lambda extraction.
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/sts-runtime-analysis-2mo4z9

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: 67574ae272

ℹ️ 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 thread fix/fixarm/own_fix.py
Comment on lines +251 to +254
if (re.search(rf"\breturn\b[^;]*\b{nm}\b", region) # returned
or re.search(rf"\b(out|ref)\s+{nm}\b", region) # passed out/ref
or re.search(rf"=\s*{nm}\s*[;,)]", region) # stored elsewhere
or "yield" in region):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject locals stored through method calls

When a disposable local is handed to a retaining API such as items.Add(p) or Register(p), this escape check still treats it as safe because it only rejects return, out/ref, direct assignments, and yield. The fixer will then wrap the rest of the block in using, disposing p at block exit even though another object may keep using it, which turns a leak finding into a use-after-dispose bug without a compiler error. For the advertised "only when the local doesn't escape" behavior, pass-as-argument/store-through-call cases need to be refused or proven safe.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex Fixed in 0088e83. _plan_local now also refuses when the local is passed as a call argument ([(,]\s*name\s*[),], e.g. items.Add(p) / Register(p)) — and additionally when the block contains any => (a closure that could capture and outlive it). Both route to local-escapes (suggest-only), so a leak is never turned into a use-after-dispose. Tests: test_local_passed_to_call_is_not_wrapped, test_local_captured_by_lambda_is_not_wrapped.


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Comment thread fix/fixarm/own_fix.py Outdated
Comment on lines +355 to +359
touched = set()
for s, e, _ in edits:
touched.update(range(s, max(e, s + 1)))
if touched & occupied:
skipped.append((f, "overlapping-edit"))

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 Allow multiple cleanup inserts at one anchor

For classes with more than one disposable field, every field disposal is anchored after the same InitializeComponent() line, but this overlap check marks pure insertions at the same index as conflicting. As a result only the first field gets a Closed/Unloaded disposal hook and the remaining field findings are skipped as overlapping-edit; the wrapper can still report ok if at least one selected finding disappeared, leaving the other leaks unfixed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@codex Fixed in 0088e83. The overlap check now distinguishes replacements from insertions: replacements still conflict on any shared line, but multiple insertions may share an anchor (e.g. several disposable fields after one InitializeComponent()). So every field in a class now gets its own disposal hook instead of all-but-first being skipped as overlapping-edit. Test: test_multiple_disposable_fields_all_disposed (2 fields → 2 hooks, no skips).


Generated by Claude Code

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@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: 2

🧹 Nitpick comments (1)
fix/tests/test_own_fix.py (1)

179-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a non-return escape regression for disposable locals.

This new coverage only exercises return p;, but the planner also rewrites locals captured by lambdas or passed as ordinary arguments today. Please add one of those cases here so the conservative OWN001 contract is enforced by tests as well.

🤖 Prompt for 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.

In `@fix/tests/test_own_fix.py` around lines 179 - 195, Add a regression test in
test_escaping_local_is_not_wrapped() that covers a disposable local escaping
without a return, such as being captured by a lambda or passed as a normal
argument, and verify plan_file() still leaves the source unchanged and marks the
OWN001 finding as skipped with local-escapes. Use the existing helpers and
symbols already in the test (test_escaping_local_is_not_wrapped, _tmp_cs,
Finding, plan_file) so the conservative escape behavior is enforced for
non-return cases too.
🤖 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 `@fix/fixarm/own_fix.py`:
- Around line 147-149: The ctor-anchor lookup in _find_ctor_anchor is scanning
past the current class and can pick up an InitializeComponent() from a later
class, so bound the search to the enclosing class scope. Update the loop that
starts from cls_idx to stop at that class’s closing brace (using the class block
boundary already available in the parser) and only return an anchor found within
that class.
- Around line 249-255: The escape detection in the local-escapes check is
incomplete, so it still allows values that can outlive the block to be treated
as safe. Update the logic in the function that scans the `region` for escape
patterns to also reject cases where the local is passed into a call or captured
by a closure/lambda/event handler, not just `return`, `out/ref`, assignment, or
`yield`. Keep the existing `local-escapes` failure path in `own_fix.py` and
broaden the heuristics so OWN001 refuses to rewrite whenever the symbol can
escape through a callee or callback.

---

Nitpick comments:
In `@fix/tests/test_own_fix.py`:
- Around line 179-195: Add a regression test in
test_escaping_local_is_not_wrapped() that covers a disposable local escaping
without a return, such as being captured by a lambda or passed as a normal
argument, and verify plan_file() still leaves the source unchanged and marks the
OWN001 finding as skipped with local-escapes. Use the existing helpers and
symbols already in the test (test_escaping_local_is_not_wrapped, _tmp_cs,
Finding, plan_file) so the conservative escape behavior is enforced for
non-return cases too.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: adb97fd3-7f06-4f09-93e7-133fad26f605

📥 Commits

Reviewing files that changed from the base of the PR and between 8c3f7b5 and 67574ae.

📒 Files selected for processing (15)
  • docs/fix-arm.md
  • fix/README.md
  • fix/fixarm/own_fix.py
  • fix/fixtures/own001-disposable-field/after.findings.json
  • fix/fixtures/own001-disposable-field/before.findings.json
  • fix/fixtures/own001-disposable-field/before/Broker/ShareWindow.xaml.cs
  • fix/fixtures/own001-disposable-local/after.findings.json
  • fix/fixtures/own001-disposable-local/before.findings.json
  • fix/fixtures/own001-disposable-local/before/Broker/Helper.cs
  • fix/fixtures/own001-lambda-extract/after.findings.json
  • fix/fixtures/own001-lambda-extract/before.findings.json
  • fix/fixtures/own001-lambda-extract/before/Broker/DatabaseOptimizationWindow.xaml.cs
  • fix/fixtures/own001-lambda/before.findings.json
  • fix/fixtures/own001-lambda/before/Broker/DatabaseOptimizationWindow.xaml.cs
  • fix/tests/test_own_fix.py

Comment thread fix/fixarm/own_fix.py Outdated
Comment thread fix/fixarm/own_fix.py
Address Codex (2) + CodeRabbit (3) review on PR #4:

- local-escapes (Codex P1 + CodeRabbit): also refuse when the local is passed as
  a call argument ('Register(p)') or when a closure is present in the block
  ('=>' -> may capture and outlive it). Prevents turning a leak into a
  use-after-dispose. (+2 tests: arg-pass, lambda-capture)
- _find_ctor_anchor (CodeRabbit): bound the InitializeComponent() search to the
  field's enclosing class via _class_close, so a later class can't anchor the
  hook in the wrong place. (+test: no-ctor-anchor when class lacks it)
- overlap check (Codex P2): allow multiple INSERTIONS at one anchor (only
  replacements conflict), so every disposable field in a class gets its hook
  instead of all-but-first skipped as overlapping-edit. (+test: 2 fields)

20/20 own + 7/7 wrapper, normal and -O.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLHz4cevQyXHKZnjzCqhoa
@PhysShell
PhysShell merged commit 7cadb72 into main Jun 25, 2026
1 check passed
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.

2 participants