Build a loop's back edge from its continue statements when the body end is unreachable - #6420
Merged
Merged
Conversation
…nd is unreachable filterOutLoopExitPoints() marks a loop body containing break or continue as not always terminating, and the loop handlers then treated the end of the body as reachable. When the body ends in return or throw, that dead scope was merged into the next iteration and into the scope after the loop, so a variable assigned on the only path that loops back came out as possibly undefined. InternalStatementResult now keeps whether the end of the statements is reachable, and getLoopBackEdgeScope() returns the scope the next iteration starts from: the reachable end merged with the continue statements, or null when no iteration follows. While, do-while, for and foreach loops, including unrolled constant-array iterations, build their convergence passes and post-loop scopes from it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LVEGd9G9w8j64EZQ7rysC
Collaborator
|
You've opened the pull request against the latest branch 2.3.x. PHPStan 2.3 is not going to be released for months. If your code is relevant on 2.2.x and you want it to be released sooner, please rebase your pull request and change its target to 2.2.x. |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014LVEGd9G9w8j64EZQ7rysC
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 phpstan/phpstan#14418
Closes phpstan/phpstan#13959
Closes phpstan/phpstan#11919
Closes phpstan/phpstan#1946
InternalStatementResult::filterOutLoopExitPoints()marks a loop body that containsbreakorcontinueas not always terminating, and the loop handlers then treated the end of the body as reachable. When the body ends inreturnorthrow, that dead scope was merged into the next iteration and into the scope after the loop. In the issue the end of thetry/catchis unreachable,TryCatchHandlerlets the scope from before thetrystand in for it, and$e, assigned on the only path that loops back, came out as possibly undefined.InternalStatementResultnow keeps whether the end of the statements is reachable.isEndReachable()is not reset byfilterOutLoopExitPoints(), andgetLoopBackEdgeScope()returns the scope the next iteration starts from: the reachable end merged with the continue statements, or null when no iteration follows. While, do-while, for and foreach loops, including the unrolled iterations of constant arrays, build their convergence passes and post-loop scopes from it. A loop whose back edge is dead is left only throughbreak.What changes for users
continueare defined after the loop when the body cannot otherwise loop back. The same holds for break-only bodies,continue 2from an inner loop or aswitch, and acatchwithfinally. Loops that may not run at all still report the variable as possibly undefined.nsrt/for-loop-i-type.phpnow infers1forfor ($i = 1; $i < 50; $i++) { break; }, where the increment never runs. The old expectation is the same on 2.2.x, so no branch had pinned it. The sharper types can surface real errors: in added regression test #1946 every iteration breaks, so$tagisnulland passing it tostrlen()is now reported.for ($i = 0; $i < 3; $i++) { exit(1); }. These reports are correct and appear only where the end of the body is already unreachable.WhileHandlernarrows continue scopes by the falsey condition too, like the end of the body.Not changed: a
continueinside atrywhosefinallyassigns a variable still reports that variable as possibly undefined after the loop, becauseTryCatchHandlerdoes not apply thefinallyto continue exit points.Tests
DefinedVariableRuleTest::testBug14418runs the sample from the issue with the defaultpolluteScopeWithLoopInitialAssignments: true. With that option off, a for loop that always runs merges the scope before the loop into the scope after it by design, so the report stays there.nsrt/bug-14418.phpcovers for, while, do-while, foreach and unrolled foreach withcontinue, break-only bodies,while (true),continue 2,continuein aswitch,catchwithfinally, and types after the loop and in the loop head. It also has the controls that must stay possibly undefined.Regression tests for the other issues the issue bot reports as fixed:
nsrt/bug-13959.php: a foreach whose body otherwise throws narrows the iterated list through itscontinuestatements only.nsrt/bug-1946.phpandCallToFunctionParametersRuleTest::testBug1946: every iteration of an unrolled foreach breaks, so$tagisnullafter the loop.CallToFunctionParametersRuleTest::testBug11919: a foreach body that always ends incontinue 2no longer runs a second iteration with the reassigned$row. It requires PHP 8, becausefgetcsv()can still returnnullbefore PHP 8.The new tests and the
for-loop-i-type.phpexpectation fail without the fix.Verification
make phpstan: no errors.phpcsand lint on the changed files: clean.The PR targets 2.3.x because the four loop handlers differ from 2.2.x by 30 to 130 lines each. This PR and #6421 both change the convergence loops. They conflict in
DoWhileHandlerandForHandler, so whichever lands second needs a rebase.🤖 Generated with Claude Code
https://claude.ai/code/session_014LVEGd9G9w8j64EZQ7rysC