Run loop convergence at all nesting depths in NodeScopeResolver#5762
Open
phpstan-bot wants to merge 1 commit into
Open
Run loop convergence at all nesting depths in NodeScopeResolver#5762phpstan-bot wants to merge 1 commit into
NodeScopeResolver#5762phpstan-bot wants to merge 1 commit into
Conversation
- Remove `if ($context->isTopLevel())` guard from convergence blocks
in all four loop types (foreach, while, do-while, for) so that
nested loops converge even when inside an outer loop's convergence
- Keep `$context->enterDeep()` in convergence body processing so that
other top-level behaviors (alwaysIterates/neverIterates detection,
early exit for always-false conditions) remain suppressed for nested
loops
- Update two test expectations that reflected the old imprecise analysis:
- generalize-scope-recursive.php: more precise recursive array type
- bug-10438.php: list<string> instead of array{}|array{string}
Closes phpstan/phpstan#10666
VincentLanglet
approved these changes
May 25, 2026
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
When array-modifying functions like
array_pop()orarray_shift()were used inside a nested loop, PHPStan incorrectly narrowed the array type toarray{}because the inner loop didn't converge during the outer loop's convergence analysis. This caused false positives like "Variable $text in empty() always exists and is always falsy."The fix removes the
if ($context->isTopLevel())guard from loop convergence blocks so that nested loops always converge, while preserving$context->enterDeep()in the convergence body to suppress other top-level behaviors.Changes
src/Analyser/NodeScopeResolver.php: Removedif ($context->isTopLevel())guard from convergence blocks in all four loop types:$context->enterDeep()is preserved unchanged in convergence body processing — this suppresses top-level-only behaviors likealwaysIterates/neverIteratesdetection and always-false-condition early exit for nested loopstests/PHPStan/Analyser/nsrt/generalize-scope-recursive.php:array{}|array{foo?: array}→array{}|array{foo: array<array>}(more precise recursive type)tests/PHPStan/Analyser/nsrt/bug-10438.php:array<string, array{}|array{string}|string>→array<string, list<string>|string>(correctly generalizes to list)Root cause
The convergence loop (do-while that iterates up to
LOOP_SCOPE_ITERATIONStimes to find stable types) was guarded byif ($context->isTopLevel()). The convergence body was processed with$context->enterDeep(), which setisTopLevel()to false. This caused nested loops to skip convergence entirely, getting only a single pass through their body.For the reported bug: the outer while loop's convergence processed its body (containing the inner while loop) with
enterDeep(). The inner while loop skipped convergence and executed only once, adding just 1 element to the array. Thenarray_pop()removed it, producingarray{}. This incorrect type propagated to the outer loop's condition scope.The fix decouples "run convergence" from "is top-level" by removing the guard. Convergence now runs at all depths, each bounded by
LOOP_SCOPE_ITERATIONS(3). TheenterDeep()in the body still suppresses other top-level behaviors, preventing regressions from nested loops getting full top-level treatment.Test
tests/PHPStan/Rules/Variables/data/bug-10666.phpwith four variants covering all combinations: while+array_pop, while+array_shift, do-while+array_pop, for+array_pop — all inside an outer while loop withempty()checktests/PHPStan/Rules/Variables/EmptyRuleTest::testBug10666()expecting no errors (false positive fix)tests/PHPStan/Analyser/nsrt/bug-10666.phpwith assertType checks verifying correct list types after pop/shift in nested loopsFixes phpstan/phpstan#10666