Mark for/while loops as never exiting when every fall-through iteration keeps the condition satisfied#5907
Open
phpstan-bot wants to merge 1 commit into
Open
Conversation
…ration keeps the condition satisfied - In NodeScopeResolver's `For_` handling, after computing the body's fall-through scope and applying the loop expressions, check whether the loop condition is still always true. If so, the loop can only ever leave via `break`/`return`/`throw`, so it is treated as always-iterating and the code after it becomes unreachable (no false `return.missing`). - Apply the same reasoning to `While_`, using the body-end scope to re-evaluate the condition. - Both checks are guarded to the top-level loop pass, require the loop to iterate at least once, and only fire when the body is not already always-terminating, so genuine missing-return cases (e.g. a catch that merely logs and continues) keep being reported. - `do-while` already handled this correctly and needed no change; `foreach` has no value-narrowable condition and is unaffected.
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.
Summary
PHPStan reported a false
Method ... should return ... but return statement is missing.error for methods whose only body is afor/whileloop containing atry { return ...; }where the boundary iteration'scatchalways terminates (e.g. amatch($i)with adefault => throw, or anif ($i >= 5) throw). In such loops the condition can never become false because the only iteration that could let it exit always returns or throws, so the code after the loop is in fact unreachable.The root cause is that loop-scope generalization widens the loop variable (
$i→int), which loses the bound that proves the loop never exits normally. As a result PHPStan thought execution could fall through the loop.Changes
src/Analyser/NodeScopeResolver.php–For_handling: after building the non-generalized$loopScope(body fall-through scope with the loop expressions applied), if the last condition is still always true there, set$alwaysIterates = TrinaryLogic::createYes(). The check is gated on$context->isTopLevel(),$isIterableAtLeastOnce->yes(), and the body not already being always-terminating.src/Analyser/NodeScopeResolver.php–While_handling: mirror the same check using$finalScopeResult->getScope()(the body-end scope, which already includes the in-body increment) to re-evaluate the condition; set$alwaysIterates = truewhen it is always true.do-while(Do_) already produced the correct result and was left unchanged;foreachhas no value-narrowable continuation condition, so it is unaffected.Root cause
After the loop-body fixpoint, the body's fall-through scope is a sound over-approximation of every non-terminating iteration. If applying the loop's continuation expression to that scope still satisfies the loop condition, then any iteration that does not
return/throwimmediately re-enters the loop — the loop can never terminate via its condition. The existing code only recognized this for literal infinite loops (while (true)), because for a counter-based condition the generalized loop variable made the conditionmayberather thantrue. The fix re-evaluates the condition on the precise (pre-generalization) post-iteration scope instead, where the in-body narrowing (match/ifwiththrow) still constrains the variable. Because the post-iteration scope is an over-approximation, the condition is only treated as always-true when it genuinely is, so no real missing-return is suppressed.Test
tests/PHPStan/Rules/Missing/data/bug-9444.php+MissingReturnRuleTest::testBug9444reproduce the reported case (forloop with amatchwhosedefaultarm throws) plus two analogous cases: aforloop withif ($i >= 5) throwand awhile ($i <= 5)variant. All three were false positives before the fix and report no error after it.for/whilewhosecatchonly logs and continues, and a loop that can exit via its condition), and that$codes-style accumulator inference inside nested loops (regression innsrt/bug-13705.php) is preserved by guarding theforcheck to the top-level pass.make tests(17434 tests),make phpstan, andmake cs-fixall pass.Fixes phpstan/phpstan#9444