Skip to content

feat(extractor): close remaining flow-lowering gaps — finally-before-return, do, switch#34

Merged
PhysShell merged 2 commits into
mainfrom
claude/zen-pasteur-76hfs1
Jun 18, 2026
Merged

feat(extractor): close remaining flow-lowering gaps — finally-before-return, do, switch#34
PhysShell merged 2 commits into
mainfrom
claude/zen-pasteur-76hfs1

Conversation

@PhysShell

@PhysShell PhysShell commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Closes the last deferred control-flow recall gaps in the --flow-locals lowering. The flow detector now models every common construct — only goto/labeled statements and a few exotic forms still honestly bail. All three changes preserve the no-false-positive invariant.

What changed

Gap How it's closed CI-pinned sample
finally-before-return An onReturn continuation is threaded alongside onThrow (finally-stack + return), so a return inside a try-with-finally runs the finally first — a finally-disposed resource is released on the return path. Unlike onThrow, onReturn is never suppressed by catches (a return is never caught). The old "bail the whole method if a try-with-finally has a return" is removed. earlyRet leaks (early return skips a later dispose); other silent (finally disposes it on the return path); tfRet stays silent
do/while Desugared do { B } while(c)B; while(c){ B } (body runs 1+ times). A bare 0+-trip while would be unsound — a resource released only in the body but acquired outside would falsely leak on the phantom 0-trip path. doLeak leaks; doClean silent
switch Lowered to a chain of opaque, mutually-exclusive if(*) branches (one per section, trailing break stripped; nested break / goto case / throw bail). With no default the last case becomes the tail rather than an empty no-match branch — avoiding a false positive on an exhaustive switch (e.g. over an enum) where every case disposes. swLeak leaks (default branch doesn't dispose); swAll silent (all cases dispose, no default)

Validation

  • Core IR pinned locally — new tests/fixtures/ownir/flow_finally_switch.facts.json encodes the exact lowered IR; test_ownir.py asserts the verdicts (clean finally-return + switch-all-dispose stay silent; early-return + switch-else leak). Suite 53/53 (was 52).
  • C# lowering — validated end-to-end in CI: new golden leak assertions (earlyRet, doLeak, swLeak) and silent-allowlist additions (other, doClean, swAll). Every existing sample keeps its verdict; the --stats invariant still holds.
  • Each new sample's lowering was hand-traced against the fixture shapes.

Notes

  • The switch no-default model trades a sound recall gap (a non-exhaustive no-match leak is missed only when every case disposes) for never false-flagging an exhaustive switch. Enum-exhaustiveness-aware modelling (using the semantic model) is a possible future refinement.

With this, the flow-lowering recall work is complete — next stop is the lifetimes / borrow model.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED


Generated by Claude Code

Summary by CodeRabbit

  • Improvements
    • Improved resource disposal analysis for try/finally paths that include early returns, reducing missed leaks.
    • Extended flow modeling to better handle switch statements and do-while loops in disposal scenarios.
    • Improved handling of null-conditional disposal (e.g., x?.Dispose()), treating it consistently with other tracked releases.
  • Testing
    • Added new IR fixtures and assertions covering finally-before-return combined with switch lowering.
  • Documentation
    • Updated roadmap and real-world mining notes to reflect expanded control-flow construct coverage.

…return, do, switch

Closes the last deferred control-flow recall gaps in the --flow-locals lowering, so the
flow detector now models every common construct (only goto/labeled statements and a few
exotic forms still honestly bail).

- finally-before-return: an `onReturn` continuation is threaded alongside `onThrow`
  (the finally-stack + return), so a `return` inside a try-with-finally runs the finally
  FIRST — a finally-disposed resource is released on the return path. Unlike `onThrow`,
  `onReturn` is never suppressed by catches (a return is never caught). The old "bail the
  whole method if a try-with-finally has a return" is removed; such methods are analysed,
  and an early return that skips a later dispose is now caught.

- do-while: desugared `do { B } while(c)` -> `B; while(c){ B }` (the body runs 1+ times).
  Modelling it as a bare 0+-trip `while` would be unsound — a resource released only in the
  body but acquired outside would falsely leak on the phantom 0-trip path.

- switch: lowered to a chain of opaque, mutually-exclusive `if(*)` branches, one per
  section, trailing `break` stripped (anything else — nested break, goto case, throw —
  bails). With NO default the LAST case becomes the tail rather than an empty no-match
  branch: that avoids a FALSE POSITIVE on an exhaustive switch (e.g. over an enum) where
  every case disposes; the cost is a sound recall gap (a non-exhaustive no-match leak is
  only missed when every case disposes).

All no-false-positive-preserving. Validated on the core IR via a new flow_finally_switch
fixture (silent: finally-before-return clean, switch-all-dispose; leak: early-return,
switch-else) — tests 53/53 — and end-to-end in CI on new samples (earlyRet/doLeak/swLeak
leak; other/doClean/swAll/tfRet silent). Existing samples keep their verdicts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rg8kSk1YT14x7A1vo5zgED
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5d4bcd80-41b7-4473-8824-5745aed213f0

📥 Commits

Reviewing files that changed from the base of the PR and between 95af05c and bb97a5d.

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

📝 Walkthrough

Walkthrough

The PR threads an onReturn continuation parameter through LowerFlowStmt and TryStatementSyntax lowering so that finally-disposal nodes execute on early-return paths. The same continuation is propagated through if, while, foreach, for, do, and switch constructs. Expression lowering is extended to recognize null-conditional disposal patterns. New sample methods, an IR fixture, unit test assertions, CI checks, and doc updates cover the added constructs.

Changes

finally/switch/do onReturn Lowering

Layer / File(s) Summary
LowerFlowStmt onReturn threading and control-flow propagation
frontend/roslyn/OwnSharp.Extractor/Program.cs
LowerFlowStmt signature gains an onReturn continuation parameter. Block lowering propagates onReturn through nested statements. if branches thread the continuation. return statements emit onReturn when non-null, otherwise a bare return. while, foreach, and for loop bodies propagate onReturn. try lowering computes a bodyOnReturn path prepending finally-nodes before an exit return node and passes it into body/do/switch-section lowering. LowerSwitchSection contract extended to accept and forward onReturn.
Null-conditional disposal expression handling
frontend/roslyn/OwnSharp.Extractor/Program.cs
EmitFlowExpr extended to recognize ConditionalAccessExpressionSyntax patterns (x?.Dispose(), x?.Close(), x?.DisposeAsync()) and emit a release for tracked locals, aligning expression-level handling with statement-level dispose-shape detection.
Sample methods for early-return, do, and switch cases
frontend/roslyn/samples/FlowLocalsSample.cs
Adds EarlyReturnLeak, NullCondFinallyDispose, DoLeak, DoClean, SwitchAllDispose, and SwitchOneLeak public methods exercising finally-before-return, null-conditional disposal in finally, do-while loop desugaring, and switch exhaustiveness. Updates the TryFinallyReturn comment to reflect the method is no longer skipped by the analyzer.
IR fixture and unit tests for finally/switch lowering
tests/fixtures/ownir/flow_finally_switch.facts.json, tests/test_ownir.py
New JSON fixture defines four function IR bodies (FinallyReturnClean, FinallyReturnLeak, SwitchAllDispose, SwitchOneLeak) with explicit acquire/release/return/use control-flow patterns. New test block asserts exactly two OWN001 findings (r2@20, s2@40) while clean finally-return and fully-disposed switch paths remain silent.
CI assertions and documentation updates
.github/workflows/ci.yml, docs/ROADMAP.md, docs/notes/real-world-mining.md
CI extends positive OWN001 checks for earlyRet, doLeak, swLeak and widens the silent/exempt allowlist with doClean, swAll, and other newly tested case identifiers. Roadmap and mining notes updated to reflect finally-before-return, do, and switch are now lowered with specific modeling behavior and remaining honest bailouts for goto/labeled/exotic forms.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PhysShell/Own.NET#18: Introduced while/foreach lowering in LowerFlowStmt; this PR extends the same lowering paths to thread onReturn through those loop bodies.
  • PhysShell/Own.NET#31: Updated CI expectations and FlowLocalsSample for try/finally leak cases; this PR extends the same CI assertion pattern for earlyRet/doLeak/swLeak and adds finally-before-return threading support.
  • PhysShell/Own.NET#33: Threaded an onThrow exception-edge continuation through TryStatementSyntax lowering in the same LowerFlowStmt code paths that this PR modifies to add onReturn.

Poem

🐇 Hop, hop through the finally gate,
The return must wait — dispose comes first, mate!
do-while desugared, switch branches aligned,
Null-conditional disposal on every find.
The rabbit checks fixtures, the CI stays green,
The cleanest of closures you ever have seen! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically summarizes the main change: closing three flow-lowering gaps (finally-before-return, do-while, switch) in the extractor's control-flow modeling.
Docstring Coverage ✅ Passed Docstring coverage is 90.00% which is sufficient. The required threshold is 80.00%.
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/zen-pasteur-76hfs1

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

@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: 95af05cfc5

ℹ️ 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".

{ new { op = "return", var = (string?)null, line = LineOf(trys) } });
foreach (var stmt in trys.Block.Statements)
if (!LowerFlowStmt(stmt, tracked, nodes, bodyCanEscape, bodyOnThrow))
if (!LowerFlowStmt(stmt, tracked, nodes, bodyCanEscape, bodyOnThrow, bodyOnReturn))

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 Handle null-conditional finally disposals before threading returns

When a try/finally containing a return uses a null-conditional dispose such as finally { r?.Dispose(); }, this new path no longer bails and threads the return through finallyNodes, but those nodes are produced by EmitFlowExpr, which only emits releases for direct member-access calls and treats r?.Dispose() as a use. The analyzer therefore reports OWN001 for a resource that the finally releases on both normal and return paths; either model conditional-access disposals before lowering these returns or keep skipping that shape.

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.

Good catch — fixed in bb97a5d. EmitFlowExpr now recognizes x?.Dispose() / x?.Close() / x?.DisposeAsync() (the member-binding form under a conditional access) as a release, mirroring IsDisposeShaped (which already handled it for the may-throw / catch checks). So finally { x?.Dispose(); } releases x on both the normal completion and the threaded return path — no false OWN001. This also closes the same latent issue for a finally { x?.Dispose(); } without a return, which the finally-before-return threading just made reachable. Pinned by a new sample NullCondFinallyDispose (ncf must stay silent in CI).


Generated by Claude Code

…e in EmitFlowExpr

`EmitFlowExpr` only treated `x.Dispose()` (member access) as a release; `x?.Dispose()`
(null-conditional — a member BINDING under a conditional access) fell through to "use".
So a `finally { x?.Dispose(); }` modelled the dispose as a use, never releasing x. The
finally-before-return threading (this PR) widened that latent false positive onto the
return path: a try-with-finally + return that disposes via `?.` was bailed before, but is
now analysed and would falsely report OWN001 (Codex review).

Fix: recognize `x?.Dispose()`/`x?.Close()`/`x?.DisposeAsync()` as a release too, mirroring
IsDisposeShaped (which already handled the member-binding form for the may-throw / catch
checks). Pinned by a new sample NullCondFinallyDispose (`ncf` must stay silent in CI).

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.

2 participants