|
19 | 19 | use PhpParser\Node\Expr\StaticCall; |
20 | 20 | use PhpParser\Node\Expr\StaticPropertyFetch; |
21 | 21 | use PhpParser\Node\Name; |
| 22 | +use PHPStan\Analyser\ExprHandler\BooleanAndHandler; |
22 | 23 | use PHPStan\DependencyInjection\AutowiredService; |
23 | 24 | use PHPStan\Node\Expr\AlwaysRememberedExpr; |
24 | 25 | use PHPStan\Node\Expr\TypeExpr; |
@@ -731,6 +732,13 @@ public function specifyTypesInCondition( |
731 | 732 | if (!$scope instanceof MutatingScope) { |
732 | 733 | throw new ShouldNotHappenException(); |
733 | 734 | } |
| 735 | + |
| 736 | + // For deep BooleanOr chains, flatten and process all arms at once |
| 737 | + // to avoid O(n^2) recursive filterByFalseyValue calls |
| 738 | + if (BooleanAndHandler::getBooleanExpressionDepth($expr) > 8) { |
| 739 | + return $this->specifyTypesForFlattenedBooleanOr($scope, $expr, $context); |
| 740 | + } |
| 741 | + |
734 | 742 | $leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr); |
735 | 743 | $rightScope = $scope->filterByFalseyValue($expr->left); |
736 | 744 | $rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr); |
@@ -1967,6 +1975,60 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes |
1967 | 1975 | return []; |
1968 | 1976 | } |
1969 | 1977 |
|
| 1978 | + /** |
| 1979 | + * Flatten a deep BooleanOr chain into leaf expressions and process them |
| 1980 | + * without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n) |
| 1981 | + * for chains with many arms (e.g., 80+ === comparisons in ||). |
| 1982 | + */ |
| 1983 | + private function specifyTypesForFlattenedBooleanOr( |
| 1984 | + MutatingScope $scope, |
| 1985 | + BooleanOr|LogicalOr $expr, |
| 1986 | + TypeSpecifierContext $context, |
| 1987 | + ): SpecifiedTypes |
| 1988 | + { |
| 1989 | + // Collect all leaf expressions from the chain |
| 1990 | + $arms = []; |
| 1991 | + $current = $expr; |
| 1992 | + while ($current instanceof BooleanOr || $current instanceof LogicalOr) { |
| 1993 | + $arms[] = $current->right; |
| 1994 | + $current = $current->left; |
| 1995 | + } |
| 1996 | + $arms[] = $current; // leftmost leaf |
| 1997 | + $arms = array_reverse($arms); |
| 1998 | + |
| 1999 | + if ($context->false() || $context->falsey()) { |
| 2000 | + // Falsey: all arms are false → union all SpecifiedTypes |
| 2001 | + $result = new SpecifiedTypes([], []); |
| 2002 | + foreach ($arms as $arm) { |
| 2003 | + $armTypes = $this->specifyTypesInCondition($scope, $arm, $context); |
| 2004 | + $result = $result->unionWith($armTypes); |
| 2005 | + } |
| 2006 | + return $result->setRootExpr($expr); |
| 2007 | + } |
| 2008 | + |
| 2009 | + // Truthy: at least one arm is true → intersect all normalized SpecifiedTypes |
| 2010 | + $armSpecifiedTypes = []; |
| 2011 | + foreach ($arms as $arm) { |
| 2012 | + $armTypes = $this->specifyTypesInCondition($scope, $arm, $context); |
| 2013 | + $armSpecifiedTypes[] = $armTypes->normalize($scope); |
| 2014 | + } |
| 2015 | + |
| 2016 | + $types = $armSpecifiedTypes[0]; |
| 2017 | + for ($i = 1; $i < count($armSpecifiedTypes); $i++) { |
| 2018 | + $types = $types->intersectWith($armSpecifiedTypes[$i]); |
| 2019 | + } |
| 2020 | + |
| 2021 | + $result = new SpecifiedTypes( |
| 2022 | + $types->getSureTypes(), |
| 2023 | + $types->getSureNotTypes(), |
| 2024 | + ); |
| 2025 | + if ($types->shouldOverwrite()) { |
| 2026 | + $result = $result->setAlwaysOverwriteTypes(); |
| 2027 | + } |
| 2028 | + |
| 2029 | + return $result->setRootExpr($expr); |
| 2030 | + } |
| 2031 | + |
1970 | 2032 | /** |
1971 | 2033 | * @return array<string, ConditionalExpressionHolder[]> |
1972 | 2034 | */ |
|
0 commit comments