Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach#6094
Open
zonuexe wants to merge 2 commits into
Open
Narrow foreach body to non-empty regardless of polluteScopeWithAlwaysIterableForeach#6094zonuexe wants to merge 2 commits into
zonuexe wants to merge 2 commits into
Conversation
…s of polluteScopeWithAlwaysIterableForeach The in-body narrowing of the iterated expression (list -> non-empty-list, array -> non-empty-array) was an accidental side effect of the polluteScopeWithAlwaysIterableForeach scope-pollution mechanism: it only happened when the flag was true. With the flag set to false (as phpstan-strict-rules does), the narrowing silently disappeared even though the loop body is provably entered only when the iteratee is non-empty. Apply the non-empty narrowing at foreach-body entry from the pre-loop scope unconditionally, mirroring the previous flag-true behaviour exactly. The flag now only controls what leaks into the after-loop scope, which is its documented purpose. Closes phpstan/phpstan#13312
The unconditional non-empty narrowing makes phpstan-src's own foreach bodies provably entered, which surfaces six latent issues under phpstan-strict-rules: - foreach.valueOverwrite: three inner loops whose value variable now shadows a definitely-defined outer variable are renamed ($internalError -> $error, $file -> $sourceFile, $i -> $j). - offsetAccess.nonArray: three [$k, $v] = $this->unsealed destructurings on a list<Type>|null property gain an explicit null guard throwing ShouldNotHappenException, matching the file's existing impossible-state style.
zonuexe
force-pushed
the
foreach-non-empty-narrowing
branch
2 times, most recently
from
July 23, 2026 13:07
568511a to
8aa4092
Compare
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#13312
This reproduces with
polluteScopeWithAlwaysIterableForeach: false, whichphpstan-strict-rulessets.Inside the loop body,
$arris provably non-empty. PHPStan already narrows it there, but only by accident: the narrowing came frompolluteScopeWithAlwaysIterableForeach, a flag whose real job is to control what leaks into the scope after the loop, not what the loop body itself sees. With the flag off, the in-body narrowing disappeared along with it.A previous fix (phpstan-src#4162) added a test for this narrowing, but only under the default
polluteScopeWithAlwaysIterableForeach: true. Underphpstan-strict-rules, which sets it tofalse, the bug remained and the issue got reopened.This PR narrows the iterated expression to non-empty at foreach-body entry unconditionally, so the flag now only affects the after-loop scope, as documented.
Note
DefinedVariableRuleTest::testBug8467closes one of its twoVariable $v might not be defined.expectations.$vis assigned in oneforeach, then read inside a second, siblingforeachover an array only populated by the first loop's body — proving that second array non-empty also proves$vdefined. Confirmed with\PHPStan\dumpType()that$v's type was always correctlyint; the removed expectation was a false positive.