Skip to content

Commit 5660bcb

Browse files
committed
autoresearch: flatten deep BooleanOr chains in TypeSpecifier to avoid O(n^2) recursion
1 parent 366d214 commit 5660bcb

File tree

1 file changed

+62
-0
lines changed

1 file changed

+62
-0
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 62 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;
@@ -731,6 +732,13 @@ public function specifyTypesInCondition(
731732
if (!$scope instanceof MutatingScope) {
732733
throw new ShouldNotHappenException();
733734
}
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+
734742
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
735743
$rightScope = $scope->filterByFalseyValue($expr->left);
736744
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
@@ -1967,6 +1975,60 @@ private function processBooleanSureConditionalTypes(Scope $scope, SpecifiedTypes
19671975
return [];
19681976
}
19691977

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+
19702032
/**
19712033
* @return array<string, ConditionalExpressionHolder[]>
19722034
*/

0 commit comments

Comments
 (0)