From 84c45e22f0e7ef011561d6098d491c94bb4a5dc8 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Sun, 22 Feb 2026 18:00:15 +0000 Subject: [PATCH] Fix conditional expression holder chain resolution regression - Iterating conditional expression holders in filterBySpecifiedTypes now loops until convergence, making it order-independent - The previous commit (3daa3128a, Fix #13303) changed mergeConditionalExpressions to put existing holders before new ones, breaking chains where a new holder must fire before an existing one - Updated dependent-variable-certainty and dependent-expression-certainty tests to reflect improved certainty resolution (Maybe -> Yes) - New regression test in tests/PHPStan/Analyser/nsrt/bug-14178.php Fixes phpstan/phpstan#14178 --- src/Analyser/MutatingScope.php | 23 ++++++---- tests/PHPStan/Analyser/nsrt/bug-14178.php | 43 +++++++++++++++++++ .../nsrt/dependent-expression-certainty.php | 2 +- .../nsrt/dependent-variable-certainty.php | 2 +- 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14178.php diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 0e3004bddaf..78292314484 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3805,16 +3805,23 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self } $conditions = []; - foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { - foreach ($conditionalExpressions as $conditionalExpression) { - foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { - if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { - continue 2; - } + $prevSpecifiedCount = -1; + while (count($specifiedExpressions) !== $prevSpecifiedCount) { + $prevSpecifiedCount = count($specifiedExpressions); + foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) { + if (array_key_exists($conditionalExprString, $conditions)) { + continue; } + foreach ($conditionalExpressions as $conditionalExpression) { + foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) { + if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) { + continue 2; + } + } - $conditions[$conditionalExprString][] = $conditionalExpression; - $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + $conditions[$conditionalExprString][] = $conditionalExpression; + $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder(); + } } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14178.php b/tests/PHPStan/Analyser/nsrt/bug-14178.php new file mode 100644 index 00000000000..c3c0035f126 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14178.php @@ -0,0 +1,43 @@ + + */ + public static function diff( + ?self $previousVersion, + ?self $newVersion, + ): array { + $previousVersionExists = $previousVersion !== null; + $newVersionExists = $newVersion !== null; + + if (!$previousVersionExists && !$newVersionExists) { + return []; + } + + if ($previousVersionExists && !$newVersionExists) { + return ['bar']; + } + + if (!$previousVersionExists) { + assertType('true', $newVersionExists); + assertType('Bug14178\\HelloWorld', $newVersion); + $result = []; + $result[] = 'foo'; + $categoryString = implode(', ', $newVersion->getSomething()); + } + + return []; + } + + /** + * @return array + */ + private function getSomething(): array { + return ['foo']; + } +} diff --git a/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php b/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php index 302d82b609d..d8596c0a21c 100644 --- a/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php +++ b/tests/PHPStan/Analyser/nsrt/dependent-expression-certainty.php @@ -153,7 +153,7 @@ function (bool $a, bool $b) { assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); if (returnsBool($b)) { - assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); // could be Yes + assertVariableCertainty(TrinaryLogic::createYes(), $foo); } if (returnsBool($a)) { diff --git a/tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php b/tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php index f84857da660..8e34c91b91e 100644 --- a/tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php +++ b/tests/PHPStan/Analyser/nsrt/dependent-variable-certainty.php @@ -149,7 +149,7 @@ function (bool $a, bool $b) { assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); if ($b) { - assertVariableCertainty(TrinaryLogic::createMaybe(), $foo); // could be Yes + assertVariableCertainty(TrinaryLogic::createYes(), $foo); } if ($a) {