From 54baab07f1a4f0be6dd645a3b3a644dc20e25ada Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:03:01 +0000 Subject: [PATCH] Do not create conditional expression when guard type overlaps with non-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 --- src/Analyser/MutatingScope.php | 9 +++ .../Analyser/nsrt/bug-14469-variants.php | 54 +++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14469.php | 19 ++++++ .../BooleanNotConstantConditionRuleTest.php | 6 ++ .../Rules/Comparison/data/bug-14469.php | 66 +++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14469-variants.php create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14469.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14469.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 122340cc27b..4d587050a65 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3649,6 +3649,15 @@ private function createConditionalExpressions( ) { continue; } + if ( + !array_key_exists($exprString, $theirExpressionTypes) + && array_key_exists($guardExprString, $theirExpressionTypes) + && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() + && $theirExpressionTypes[$guardExprString]->getType()->isObject()->no() + && !$guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType())->no() + ) { + continue; + } $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder); $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14469-variants.php b/tests/PHPStan/Analyser/nsrt/bug-14469-variants.php new file mode 100644 index 00000000000..b58ca971f7a --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14469-variants.php @@ -0,0 +1,54 @@ +id === 10 ? 2 : null; + } elseif ($R['aa']) { + $aa = $R['aa']; + } + + if ($aa) { + assertType('mixed', $R['aa']); + } +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 3cf3f2a8bbc..ee1d1ff12fe 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -236,4 +236,10 @@ public function testBug6702(): void $this->analyse([__DIR__ . '/data/bug-6702.php'], []); } + public function testBug14469(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14469.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14469.php b/tests/PHPStan/Rules/Comparison/data/bug-14469.php new file mode 100644 index 00000000000..d5aea39bee6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14469.php @@ -0,0 +1,66 @@ +id === 10 ? 2 : null; + } elseif ($R['aa']) { + $aa = $R['aa']; + } + + if ($aa) { + if (!$R['aa']) { + return []; + } + } + return $R; +} + +function t2(array $R, bool $var1, object $user): array { + $aa = null; + + if ($var1) { + $aa = $user->id === 10 ? 2 : null; + } elseif ($R['aa']) { + $aa = $R['aa']; + } + + if ($aa) { + if ($R['aa'] === false) { + return []; + } + } + return $R; +} + +function t3(array $R, bool $var1, int $other): array { + $aa = null; + + if ($var1) { + $aa = $other; + } elseif ($R['bb']) { + $aa = $R['bb']; + } + + if ($aa) { + if (!$R['bb']) { + return []; + } + } + return $R; +} + +function t4(array $R, bool $var1, object $user): string { + $aa = null; + + if ($var1) { + $aa = $user->id === 10 ? 2 : null; + } elseif ($R['aa']) { + $aa = $R['aa']; + } + + return $aa ? ($R['aa'] ? 'yes' : 'no') : 'none'; +}