Do not create conditional expression when guard type overlaps with non-object type in other branch#5466
Closed
phpstan-bot wants to merge 1 commit intophpstan:2.1.xfrom
Closed
Conversation
…n-object type in other branch - In `MutatingScope::createConditionalExpressions()`, add a second skip condition for cases where the target expression does not exist in the other branch but the guard type overlaps with the other branch's guard value (for non-object types where `isSuperTypeOf` gives precise answers) - This prevents false positive "always true/false" reports when a variable is assigned from an array access in one branch and from an independent expression in another branch, and the shared variable's truthiness is later checked - The existing skip condition (from bug-14411 fix) only handles the case where the target exists in both branches; this extends it to handle the case where the target only exists in one branch - The `isObject()->no()` guard ensures we only apply this for non-object types where `isSuperTypeOf` overlap detection is reliable (object types can give imprecise Maybe results due to theoretical subclass overlap) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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
Fixes a false positive "Negated boolean expression is always false" when a variable is assigned from an array access in an
elseifbranch and from an independent expression in theifbranch, and the variable's truthiness is later checked.Changes
Added a new skip condition in
MutatingScope::createConditionalExpressions()(src/Analyser/MutatingScope.php) that prevents creating a conditional expression when:isSuperTypeOfgives precise overlap results)Added regression test in
tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.phpandtests/PHPStan/Rules/Comparison/data/bug-14469.phpwith the exact reproduction case and variants (strict comparison, different array key, ternary expression)Added NSRT tests in
tests/PHPStan/Analyser/nsrt/bug-14469.phpandtests/PHPStan/Analyser/nsrt/bug-14469-variants.phpto verify correct type inference for array accesses inside truthiness checks after if/elseif blocks, including variants with:if/elsewith nested conditionselseifbranchesRoot cause
When merging scopes from if/elseif branches,
createConditionalExpressions()creates conditional expressions that link variable types across branches. For example, if$aa = $R['aa']in the elseif branch (where$R['aa']is truthy), a conditional expression "if$aais truthy, then$R['aa']is truthy" gets created.This conditional expression is invalid when the if-branch assigns
$aafrom an independent source (e.g.,$aa = $user->id === 10 ? 2 : null) — the value2is truthy but says nothing about$R['aa']. The existing skip condition (added for bug-14411) only checked when both the target and guard exist in the other branch. This fix extends it to also check when the target does NOT exist in the other branch but the guard type overlaps with the other branch's guard type.The
isObject()->no()guard is necessary becauseisSuperTypeOfgives imprecise "Maybe" results for object/interface types due to theoretical subclass relationships (e.g.,Event->isSuperTypeOf(OrderInterface)returns Maybe even when the branches are semantically disjoint frominstanceofnarrowing). For non-object types (scalars, mixed, etc.),isSuperTypeOfgives precise overlap information.Analogous cases probed
MutatingScopeapplies to all of them.=== false): Tested and passes (variant in test data).Test
tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest::testBug14469— verifies no false positive for the exact reported case and three variantstests/PHPStan/Analyser/nsrt/bug-14469.php— verifies$R['aa']has typemixed(not truthy) insideif ($aa)blocktests/PHPStan/Analyser/nsrt/bug-14469-variants.php— verifies correct type inference for three control flow variants (if/else, multiple elseif, nested array)Fixes phpstan/phpstan#14469