From d6ff13190c5840547c50dabcbd8c663ea58ef544 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:06:32 +0000 Subject: [PATCH] Fix phpstan/phpstan#13591: Type narrowing not working with conditional exception validation - When a compound condition like `$x === null && ($y === 'a' || $y === 'b')` throws, the negation creates conditional holders with union condition types (e.g. $y: 'a'|'b') - These union conditions could never be matched exactly by later narrowing like `$y === 'a'` - Fix: expand union condition types in conditional holders into separate holders for each union member, while keeping the original union holder for exact matches - New regression test in tests/PHPStan/Analyser/nsrt/bug-13591.php --- src/Analyser/TypeSpecifier.php | 51 ++++++++++++++++++----- tests/PHPStan/Analyser/nsrt/bug-13591.php | 16 +++++++ 2 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-13591.php diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 4f3e7f9c43..f50bacc339 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -1964,11 +1964,15 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes continue; } - $holder = new ConditionalExpressionHolder( - $conditions, - ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($scope->getType($expr), $type)), - ); - $holders[$exprString][$holder->getKey()] = $holder; + $resultTypeHolder = ExpressionTypeHolder::createYes($expr, TypeCombinator::intersect($scope->getType($expr), $type)); + + foreach ($this->expandUnionConditions($conditions) as $expandedConditions) { + $holder = new ConditionalExpressionHolder( + $expandedConditions, + $resultTypeHolder, + ); + $holders[$exprString][$holder->getKey()] = $holder; + } } return $holders; @@ -2085,11 +2089,15 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy continue; } - $holder = new ConditionalExpressionHolder( - $conditions, - ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($scope->getType($expr), $type)), - ); - $holders[$exprString][$holder->getKey()] = $holder; + $resultTypeHolder = ExpressionTypeHolder::createYes($expr, TypeCombinator::remove($scope->getType($expr), $type)); + + foreach ($this->expandUnionConditions($conditions) as $expandedConditions) { + $holder = new ConditionalExpressionHolder( + $expandedConditions, + $resultTypeHolder, + ); + $holders[$exprString][$holder->getKey()] = $holder; + } } return $holders; @@ -2098,6 +2106,29 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy return []; } + /** + * @param array $conditions + * @return list> + */ + private function expandUnionConditions(array $conditions): array + { + $result = [$conditions]; + foreach ($conditions as $exprString => $typeHolder) { + $type = $typeHolder->getType(); + if (!$type instanceof UnionType) { + continue; + } + + foreach ($type->getTypes() as $innerType) { + $expanded = $conditions; + $expanded[$exprString] = ExpressionTypeHolder::createYes($typeHolder->getExpr(), $innerType); + $result[] = $expanded; + } + } + + return $result; + } + /** * @return array{Expr, ConstantScalarType, Type}|null */ diff --git a/tests/PHPStan/Analyser/nsrt/bug-13591.php b/tests/PHPStan/Analyser/nsrt/bug-13591.php new file mode 100644 index 0000000000..fea4eafdb0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13591.php @@ -0,0 +1,16 @@ +