From 5ab52209951b865041a6066449943b2ce4d072e5 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:28:47 +0000 Subject: [PATCH 1/2] Skip conditional expression guard when guard type is a subtype of the other branch's value - In MutatingScope::createConditionalExpressions(), a guard `expr=G implies target=T` was created whenever the merged type differed from the truthy value, and only skipped when the guard type was a supertype of the other (falsey) branch's value for that expression. - This missed the symmetric, equally non-discriminating case: when the guard type is a *subtype* of the other branch's value (the falsey branch still contains the guard value), the guard cannot uniquely identify the truthy branch, so the conditional is unsound. - Add the mirror check `$theirExprIsSuperTypeOfGuard->yes()` alongside the existing `$guardIsSuperTypeOfTheirExpr->yes()` so such guards are skipped. - Fixes a false `booleanAnd.alwaysFalse` / `identical.alwaysFalse` when a 2-case enum narrowed with `!==` is combined via `&&` with an object property comparison and the same enum condition is reused in a later `if`: the property narrowing from the first `if` no longer leaks into the second. --- src/Analyser/MutatingScope.php | 2 ++ tests/PHPStan/Analyser/nsrt/bug-14807.php | 33 +++++++++++++++++++ .../BooleanAndConstantConditionRuleTest.php | 6 ++++ .../Rules/Comparison/data/bug-14807.php | 25 ++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14807.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-14807.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0980fb6936f..c81d35e98fd 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3861,9 +3861,11 @@ private function createConditionalExpressions( && $theirExpressionTypes[$guardExprString]->getCertainty()->yes() ) { $guardIsSuperTypeOfTheirExpr = $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType()); + $theirExprIsSuperTypeOfGuard = $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType()); if ( $guardIsSuperTypeOfTheirExpr->yes() + || $theirExprIsSuperTypeOfGuard->yes() || ( array_key_exists($exprString, $theirExpressionTypes) && $theirExpressionTypes[$exprString]->getCertainty()->yes() diff --git a/tests/PHPStan/Analyser/nsrt/bug-14807.php b/tests/PHPStan/Analyser/nsrt/bug-14807.php new file mode 100644 index 00000000000..a2a68b77cad --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14807.php @@ -0,0 +1,33 @@ +ready === true) { + assertType('true', $item->ready); + } + + // The narrowing from the first `if` must not leak here: reusing the same + // enum left-hand condition does not imply $item->ready is still true. + if ($color !== Color::Red) { + assertType('bool', $item->ready); + } + + if ($color !== Color::Red && $item->ready === false) { + assertType('false', $item->ready); + } +} diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 1097a8289cf..3f46890d5b5 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -451,6 +451,12 @@ public function testBug8555(): void $this->analyse([__DIR__ . '/data/bug-8555.php'], []); } + public function testBug14807(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-14807.php'], []); + } + public function testInTrait(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14807.php b/tests/PHPStan/Rules/Comparison/data/bug-14807.php new file mode 100644 index 00000000000..e6a43eee503 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-14807.php @@ -0,0 +1,25 @@ +ready === true) { + echo 'go'; + } + + if ($color !== Color::Red && $item->ready === false) { + throw new \RuntimeException('stop'); + } +} From 6a8da51c6216d4a946ec968365be6ee6cec9ec05 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Thu, 11 Jun 2026 09:40:34 +0000 Subject: [PATCH 2/2] Add '// lint >= 8.1' directive to bug-14807 test data files The enum syntax requires PHP 8.1, so the test data files need the lint directive on the first line with declare() moved to the third line. Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Analyser/nsrt/bug-14807.php | 4 +++- tests/PHPStan/Rules/Comparison/data/bug-14807.php | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14807.php b/tests/PHPStan/Analyser/nsrt/bug-14807.php index a2a68b77cad..210fd6937bb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14807.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14807.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace Bug14807; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-14807.php b/tests/PHPStan/Rules/Comparison/data/bug-14807.php index e6a43eee503..63d35e69c1f 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-14807.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-14807.php @@ -1,4 +1,6 @@ -= 8.1 + +declare(strict_types = 1); namespace BugRule14807;