Do not let conditional expressions unset variables with proven Yes certainty in filterBySpecifiedTypes#5475
Open
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Open
Conversation
…rtainty in `filterBySpecifiedTypes` - In `MutatingScope::filterBySpecifiedTypes()`, when conditional expressions resolve to `no()` certainty for a variable, skip the unset if the variable already has `yes()` certainty in the current scope. - This prevents stale conditional expressions (created during earlier scope merges) from overriding definite variable existence proven by `isset()` or direct assignment. - The bug manifested when a `foreach` loop's zero-iterations scope merge triggered conditional expressions linking a variable's existence to the iterable being non-empty, even though `isset()` had already proven the variable was defined. - The same bug also affected `if` branches that narrowed the iterable type to empty.
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 a variable's existence was proven via
isset(), subsequentforeachloops (orifbranches) that triggered conditional expression resolution could incorrectly unset the variable, causing a false positive "Variable might not be defined" error.Changes
src/Analyser/MutatingScope.phpinfilterBySpecifiedTypes(): when conditional expressions resolve tono()certainty, skip the unset if the variable already hasyes()certainty in the current scopetests/PHPStan/Rules/Variables/data/bug-14353.phpcovering theforeachcase and theifbranching caseRoot cause
When
filterBySpecifiedTypes()processes conditional expressions (lines ~3263-3286), expressions whose conditional certainty resolves tono()are completely removed fromexpressionTypesviaunset(). This is correct for variables whose certainty ismaybe()orno(), but incorrect for variables withyes()certainty — theyes()certainty represents a proven fact (fromisset(), direct assignment, etc.) that should not be overridden by conditional reasoning.The conditional expressions are created during scope merges (e.g., after
foreachloops where a variable is assigned conditionally). They encode relationships like "if the iterable is empty, this variable doesn't exist." Afterisset()confirms the variable exists, these conditional expressions become stale but remain on the scope. When a later type narrowing (like$reports === []in a foreach zero-iterations merge or anifbranch) triggers the guard condition, the conditional expression fires and incorrectly removes the variable.Analogous cases probed:
whileloops: not affected (the condition-based merge doesn't trigger the same conditional expression pattern)if/elsebranching on the iterable: was affected and is now fixed by the same change, covered by the second test case!isset()(falsy branch): not affected (usesunsetExpressiondirectly, not conditional expressions)isset()guard: not affected (they havemaybe()certainty, notyes(), so the fix doesn't apply)Test
testBug14353inDefinedVariableRuleTest: expects no errors for the original reproduction case (foreach after isset) and for the if-branch case (branching on$reports === []after isset)Fixes phpstan/phpstan#14353