Skip to content

Commit 79212f5

Browse files
SanderMullerondrejmirtes
authored andcommitted
autoresearch: flatten deep BooleanOr chains in TypeSpecifier to avoid O(n^2) recursion
1 parent 43fca98 commit 79212f5

File tree

1 file changed

+64
-0
lines changed

1 file changed

+64
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PhpParser\Node\Expr\StaticCall;
2020
use PhpParser\Node\Expr\StaticPropertyFetch;
2121
use PhpParser\Node\Name;
22+
use PHPStan\Analyser\ExprHandler\BooleanAndHandler;
2223
use PHPStan\DependencyInjection\AutowiredService;
2324
use PHPStan\Node\Expr\AlwaysRememberedExpr;
2425
use PHPStan\Node\Expr\TypeExpr;
@@ -99,6 +100,8 @@ final class TypeSpecifier
99100

100101
private const MAX_ACCESSORIES_LIMIT = 8;
101102

103+
private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
104+
102105
/** @var MethodTypeSpecifyingExtension[][]|null */
103106
private ?array $methodTypeSpecifyingExtensionsByClass = null;
104107

@@ -731,6 +734,13 @@ public function specifyTypesInCondition(
731734
if (!$scope instanceof MutatingScope) {
732735
throw new ShouldNotHappenException();
733736
}
737+
738+
// For deep BooleanOr chains, flatten and process all arms at once
739+
// to avoid O(n^2) recursive filterByFalseyValue calls
740+
if (BooleanAndHandler::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
741+
return $this->specifyTypesForFlattenedBooleanOr($scope, $expr, $context);
742+
}
743+
734744
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
735745
$rightScope = $scope->filterByFalseyValue($expr->left);
736746
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
@@ -1967,6 +1977,60 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
19671977
return [];
19681978
}
19691979

1980+
/**
1981+
* Flatten a deep BooleanOr chain into leaf expressions and process them
1982+
* without recursive filterByFalseyValue calls. This reduces O(n^2) to O(n)
1983+
* for chains with many arms (e.g., 80+ === comparisons in ||).
1984+
*/
1985+
private function specifyTypesForFlattenedBooleanOr(
1986+
MutatingScope $scope,
1987+
BooleanOr|LogicalOr $expr,
1988+
TypeSpecifierContext $context,
1989+
): SpecifiedTypes
1990+
{
1991+
// Collect all leaf expressions from the chain
1992+
$arms = [];
1993+
$current = $expr;
1994+
while ($current instanceof BooleanOr || $current instanceof LogicalOr) {
1995+
$arms[] = $current->right;
1996+
$current = $current->left;
1997+
}
1998+
$arms[] = $current; // leftmost leaf
1999+
$arms = array_reverse($arms);
2000+
2001+
if ($context->false() || $context->falsey()) {
2002+
// Falsey: all arms are false → union all SpecifiedTypes
2003+
$result = new SpecifiedTypes([], []);
2004+
foreach ($arms as $arm) {
2005+
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2006+
$result = $result->unionWith($armTypes);
2007+
}
2008+
return $result->setRootExpr($expr);
2009+
}
2010+
2011+
// Truthy: at least one arm is true → intersect all normalized SpecifiedTypes
2012+
$armSpecifiedTypes = [];
2013+
foreach ($arms as $arm) {
2014+
$armTypes = $this->specifyTypesInCondition($scope, $arm, $context);
2015+
$armSpecifiedTypes[] = $armTypes->normalize($scope);
2016+
}
2017+
2018+
$types = $armSpecifiedTypes[0];
2019+
for ($i = 1; $i < count($armSpecifiedTypes); $i++) {
2020+
$types = $types->intersectWith($armSpecifiedTypes[$i]);
2021+
}
2022+
2023+
$result = new SpecifiedTypes(
2024+
$types->getSureTypes(),
2025+
$types->getSureNotTypes(),
2026+
);
2027+
if ($types->shouldOverwrite()) {
2028+
$result = $result->setAlwaysOverwriteTypes();
2029+
}
2030+
2031+
return $result->setRootExpr($expr);
2032+
}
2033+
19702034
/**
19712035
* @return array<string, ConditionalExpressionHolder[]>
19722036
*/

0 commit comments

Comments
 (0)