Do not create conditional expressions for non-Variable expressions absent from the other branch#5465
Conversation
…sent from the other branch - In `MutatingScope::createConditionalExpressions()`, skip creating a conditional expression when the target expression (e.g. an array dim fetch like `$R['aa']`) is not tracked in the other branch, the expression is not a Variable, and the guard type overlaps with the other branch's guard type. - This prevents incorrect type narrowing where an array dim fetch or property fetch from an `elseif` condition leaks into a subsequent `if` block that checks a variable set in multiple branches. - The Variable exception preserves the existing behavior for variable certainty tracking (e.g. a variable defined in only one branch correctly gets Yes certainty via conditional expressions). - Tested with array dim fetch, nested array dim fetch, property fetch, and multiple elseif variants.
| } | ||
| if ( | ||
| !array_key_exists($exprString, $theirExpressionTypes) | ||
| && !$holder->getExpr() instanceof Variable |
There was a problem hiding this comment.
Why is it restricted for !$holder->getExpr() instanceof Variable ?
Can't the same bug occurs in that case ?
There was a problem hiding this comment.
The changes have been pushed. Here's a summary of what was done to address VincentLanglet's review comment:
The reviewer's question: Why is the check restricted to !$holder->getExpr() instanceof Variable? Can't the same bug occur for Variables?
Investigation and answer:
I tested the Variable case empirically:
-
Pre-defined Variables (defined before the if/elseif, present in both branches' expression types): These are handled correctly by the existing check at lines 3644-3651 which tests
array_key_exists($exprString, $theirExpressionTypes). I verified this with a variable-equivalent test case ($bb = $inputbefore the if/elseif) - no false positive occurs. -
Newly-defined Variables (only defined in one branch, NOT in
$theirExpressionTypes): Removing the Variable exception causes a regression inbug-14411-regression.phpwhere$ordermust be recognized as always-defined when two conditions ($event instanceof Eventand$event instanceof OrderInterface) together cover all cases. Conditional expressions for newly-defined variables are essential for this certainty tracking.
The fundamental difference is that non-Variable expressions are "always queryable" (absence from a branch means "not narrowed"), while Variables have genuine defined/undefined semantics (absence means "not defined").
Changes made:
- Added a code comment explaining the Variable exception and why it's needed
- Added variable-equivalent test cases to both the rule test data and NSRT test to demonstrate correctness
… test cases Address review comment by VincentLanglet asking why the guard is restricted to non-Variable expressions. The Variable exception is needed because: - Non-Variable expressions (array dim fetches, property fetches) are always queryable; absence from a branch means "not narrowed", not "doesn't exist" - Variables are different: absence means genuinely "not defined", and conditional expressions for newly-defined variables are essential for variable certainty tracking (e.g. bug-14411-regression where $order must be recognised as always-defined when two conditions cover all cases) - Pre-defined variables (present in both branches) are already handled by the existing check that tests array_key_exists($exprString, $theirExpressionTypes) Added variable-equivalent test cases to both the rule test data and NSRT test to demonstrate that pre-defined variables work correctly with the existing guard-overlap check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
left a comment
There was a problem hiding this comment.
Remove the comments.
VincentLanglet
left a comment
There was a problem hiding this comment.
In tests files added, use @param mixed rather than native type in order to pass test on PHP 7.4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what was addressed:
Both |
…ove comments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what was changed:
All tests pass (11857 tests, 79365 assertions) and |
Summary
False positive "Negated boolean expression is always false" when checking an array dim fetch inside an
ifblock that tests a variable set from different sources inif/elseifbranches. PHPStan incorrectly concluded that$R['aa']was always truthy insideif ($aa), even though$aacould be truthy from theifbranch (where$R['aa']was never accessed).Changes
Added a guard in
MutatingScope::createConditionalExpressions()(src/Analyser/MutatingScope.php) that prevents creating conditional expressions for non-Variable expressions when:VariablenodeYesisSuperTypeOfis notno)Added regression test data at
tests/PHPStan/Rules/Comparison/data/bug-14469.phpcovering:Added NSRT test at
tests/PHPStan/Analyser/nsrt/bug-14469.phpverifying the correct type inferenceRoot cause
When
MutatingScope::mergeWith()merges two branch scopes and one branch tracks an expression (e.g.$R['aa']narrowed by anelseifcondition) while the other does not,createConditionalExpressions()would create a conditional expression linking the guard variable ($aa) to the untracked expression ($R['aa']). When the guard later fires (e.g.if ($aa)narrows$aato truthy), the conditional expression would incorrectly narrow$R['aa']to its branch-specific type, even though the guard could have been satisfied by a value from the branch where$R['aa']was never tracked.The fix adds a Variable exception because Variables can be defined/undefined across branches (their certainty is meaningful for conditional expressions), while non-Variable expressions like array dim fetches and property fetches are always queryable and should not have their type artificially narrowed by guard expressions from branches where they weren't referenced.
Analogous cases probed and found already correct:
Test
testBug14469inBooleanNotConstantConditionRuleTestwith test data covering the original array dim fetch case plus property fetch, nested array fetch, and multiple elseif variantsbug-14469.phpverifying$R['aa']is correctly inferred asmixedinsideif ($aa)Fixes phpstan/phpstan#14469