From e12a82666e0228e772a8782c93e5cd2d1fa652dc Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:46:32 +0000 Subject: [PATCH 1/2] Skip degenerate no-op antecedents when building boolean conditional expression holders - `ConditionalExpressionHolderHelper::processBooleanConditionalTypes()` builds conditional holders ("if antecedent narrows to X then consequent narrows to Y") that project a boolean operand's narrowing into later scopes. - The `sureNotTypes` branch added a condition even when the antecedent narrowing was a no-op (`intersect(scopeType, $type)` equal to the current scope type, e.g. `$a = mixed`). A trivially-true antecedent makes the implication fire unconditionally, so re-specifying the antecedent variable later (e.g. in an `elseif ($a == 3)`) wrongly applied the consequent and narrowed an unrelated variable (`$b` to `mixed~true`). - Mirror the guard already present in the `sureTypes` branch: skip the condition when the computed condition type equals the existing scope type. - This covers both `&&` and `||` since `BooleanAndHandler` and `BooleanOrHandler` share the helper. The fix is not specific to `in_array()`: the same degenerate holder was produced by plain loose comparisons such as `($a == 1 || $a == 2)`. - Legitimate conditional holders (e.g. strict `in_array($a, [1, 2], true)` whose antecedent genuinely narrows `$a`) still fire as before. --- .../ConditionalExpressionHolderHelper.php | 8 ++- tests/PHPStan/Analyser/nsrt/bug-14878.php | 71 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14878.php diff --git a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php index e8c1008c183..f2a3412c599 100644 --- a/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php +++ b/src/Analyser/ExprHandler/Helper/ConditionalExpressionHolderHelper.php @@ -172,9 +172,15 @@ public function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $con continue; } + $scopeType = $scope->getType($expr); + $conditionType = TypeCombinator::intersect($scopeType, $type); + if ($scopeType->equals($conditionType)) { + continue; + } + $conditionExpressionTypes[$exprString] = ExpressionTypeHolder::createYes( $expr, - TypeCombinator::intersect($scope->getType($expr), $type), + $conditionType, ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14878.php b/tests/PHPStan/Analyser/nsrt/bug-14878.php new file mode 100644 index 00000000000..4a8286f893e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14878.php @@ -0,0 +1,71 @@ + Date: Mon, 29 Jun 2026 12:32:16 +0000 Subject: [PATCH 2/2] Guard bug-14878 against rule errors from BooleanAnd and StrictComparison rules Reference the regression test from BooleanAndConstantConditionRuleTest and StrictComparisonOfDifferentTypesRuleTest, and add the `$b === true` / `$b === true && $cond` statements inside the branches that previously emitted "Result of && is always false" and "Strict comparison ... will always evaluate to false" so those rule tests genuinely guard the fix. Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Analyser/nsrt/bug-14878.php | 26 ++++++++++++++++--- .../BooleanAndConstantConditionRuleTest.php | 6 +++++ ...rictComparisonOfDifferentTypesRuleTest.php | 5 ++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14878.php b/tests/PHPStan/Analyser/nsrt/bug-14878.php index 4a8286f893e..dab39e4e12a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14878.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14878.php @@ -4,7 +4,13 @@ use function PHPStan\Testing\assertType; -function test($a, $b): void +// The `$b === true` and `$b === true && $cond` statements inside the branches +// are what regressed: with the bug $b was narrowed to `mixed~true`, so they +// emitted "Strict comparison ... will always evaluate to false" and "Result of +// && is always false". They are referenced from BooleanAndConstantConditionRuleTest +// and StrictComparisonOfDifferentTypesRuleTest as a regression guard. + +function test($a, $b, $cond): void { if ( in_array($a, [1, 2]) @@ -14,10 +20,13 @@ function test($a, $b): void } elseif ( $a == 3) { assertType('mixed', $b); + + $b === true; + $result = $b === true && $cond; } } -function testStrictElseIf($a, $b): void +function testStrictElseIf($a, $b, $cond): void { if ( in_array($a, [1, 2]) @@ -27,10 +36,13 @@ function testStrictElseIf($a, $b): void } elseif ( $a === 3) { assertType('mixed', $b); + + $b === true; + $result = $b === true && $cond; } } -function testPlainElse($a, $b): void +function testPlainElse($a, $b, $cond): void { if ( in_array($a, [1, 2]) @@ -39,12 +51,15 @@ function testPlainElse($a, $b): void } else { assertType('mixed', $b); + + $b === true; + $result = $b === true && $cond; } } // Same degenerate-condition pattern without in_array: a loose `==` whose // falsey narrowing of $a is a no-op on a `mixed` type. -function testLooseEqual($a, $b): void +function testLooseEqual($a, $b, $cond): void { if ( ($a == 1 || $a == 2) @@ -53,6 +68,9 @@ function testLooseEqual($a, $b): void } elseif ($a == 3) { assertType('mixed', $b); + + $b === true; + $result = $b === true && $cond; } } diff --git a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php index 77e727ecb5d..fea58e7d61d 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanAndConstantConditionRuleTest.php @@ -484,6 +484,12 @@ public function testBug14807(): void $this->analyse([__DIR__ . '/data/bug-14807.php'], []); } + public function testBug14878(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14878.php'], []); + } + public function testInTrait(): void { $this->treatPhpDocTypesAsCertain = true; diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index fc774b404e8..3da1fd10bed 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -1244,6 +1244,11 @@ public function testBug14791(): void $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14791.php'], []); } + public function testBug14878(): void + { + $this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-14878.php'], []); + } + public function testBug14847(): void { $this->analyse([__DIR__ . '/data/bug-14847.php'], [