Repository navigation
Walk a finally block with the paths that returned early from its try - #486
Merged
Merged
Conversation
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
phcorp
force-pushed
the
issue-484-finally-early-return
branch
9 times, most recently
from
October 6, 2026 13:58
ee32412 to
e391c79
Compare
phcorp
force-pushed
the
issue-484-finally-early-return
branch
3 times, most recently
from
October 7, 2026 02:36
5a30a39 to
785c5ed
Compare
A finally block runs on the way out of a return in its try body too, but
the full walk gave it only the state the try body leaves at its end, so a
guard that returned early (`if (null !== $tool) { return; }`) left the
block reading `$tool` as null, and a check that only passes on the
returned path as dead code. When the try has a finally, its returns are
collected; the block is walked once more with them joined in, last, so its
statements are read with them, while the code after the statement carries
on from the paths that did not return. The collected returns are recorded
again for an enclosing closure's by-reference captures.
Closes PHPantom-dev#484.
Walking a finally block a second time for the paths that returned early doubled per nesting level. Only the walk that records diagnostic snapshots now takes the second walk, with the carry-on walk recording nothing and the second walk recording no return edges, so a block nested n deep is walked n + 1 times. A return or rethrow inside a catch now reaches the finally block too, and hover and completion inside the block see the early returns the way diagnostics do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AJenbo
force-pushed
the
issue-484-finally-early-return
branch
from
November 3, 2026 02:07
785c5ed to
1bb0c1e
Compare
Contributor
|
Looks like there's a lot of edge cases around this, thanks for working on it |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #484.
After
if (null !== $tool) { return; }in atrybody, anull !== $toolcheck in itsfinallyblock was treated as dead code, and$tool->editable()inside it reportedCannot access method 'editable' on type 'never'('null'on 0.10.0).Cause.
process_try(src/type_engine/variable/forward_walk/control_flow.rs), on the path the full diagnostic walk takes, walked thefinallyblock with the state thetrybody leaves at its end, joined with the reachablecatches. Areturninside thetrybody is a way out of it too, and thefinallyblock runs on that way out, but its state never reached the block.Fix. When the
tryhas afinally, thetrybody is walked inside a return frame (push_return_frame, the mechanism closures already use for their by-reference captures), which collects the state each reachablereturnleaves with. Thefinallyblock is walked as before for the code after the statement, then once more with the returned paths joined in. That second walk goes last, so its snapshots are the ones the block's own statements are read with, while the scope after the statement still comes from the paths that did not return. The collected returns are recorded again for an enclosing frame, so a closure's by-reference capture written just before such areturnstill reaches its caller. Atrywithoutfinallyis untouched. Not covered here: areturninside acatch, which runs thefinallyblock too.Tests (
tests/integration/narrowing_flow_tracking.rs, full diagnostic pipeline): the issue's repro, on a property and on a local, reports nothing (two errors onmain); the code after such atrystill reads the variable asnulland reports the one error it should; a closure writing a by-reference capture before areturninside atrystill leaves'early'in the caller's type, which fails if the returns are not recorded again for the enclosing frame. Porta's ownIde.php, where this was found, analyses clean.Checks:
cargo fmt --check,cargo clippy --all-targets -- -D warnings,cargo test(all suites and doc tests), theexamples/phplint and assertions, andanalyze --project-root examples/laravelreporting exactly the 3 documented errors.Every executable line the patch adds under
src/is covered (cargo llvm-cov, 11/11).Checklist
If applicable:
CHANGELOG.mdREADME.md,docs/,examples/) — not applicable: a fix, no documented behaviour changesconfig-schema.json) — not applicable: no configurationhow it works, how it's organized), including any code drafted by an LLM.
an eye towards deleting anything that is irrelevant, clarifying anything
that is confusing, and adding details that are relevant. This includes,
for example, commit descriptions, PR descriptions, and code comments.