Skip to content

Commit 1142992

Browse files
committed
Loops analysed in linear time instead of exponential time
1 parent e4a5e98 commit 1142992

File tree

4 files changed

+87
-30
lines changed

4 files changed

+87
-30
lines changed

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1325,7 +1325,7 @@ private function resolveType(string $exprString, Expr $node): Type
13251325
}
13261326

13271327
$closureYieldStatements[] = [$node, $scope];
1328-
});
1328+
}, StatementContext::createTopLevel());
13291329

13301330
$returnTypes = [];
13311331
$hasNull = false;

src/Analyser/NodeScopeResolver.php

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ public function processNodes(
246246
continue;
247247
}
248248

249-
$statementResult = $this->processStmtNode($node, $scope, $nodeCallback);
249+
$statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel());
250250
$scope = $statementResult->getScope();
251251
if (!$statementResult->isAlwaysTerminating()) {
252252
continue;
@@ -274,8 +274,12 @@ public function processStmtNodes(
274274
array $stmts,
275275
MutatingScope $scope,
276276
callable $nodeCallback,
277+
?StatementContext $context = null,
277278
): StatementResult
278279
{
280+
if ($context === null) {
281+
$context = StatementContext::createTopLevel();
282+
}
279283
$exitPoints = [];
280284
$throwPoints = [];
281285
$alreadyTerminated = false;
@@ -290,6 +294,7 @@ public function processStmtNodes(
290294
$stmt,
291295
$scope,
292296
$nodeCallback,
297+
$context,
293298
);
294299
$scope = $statementResult->getScope();
295300
$hasYield = $hasYield || $statementResult->hasYield();
@@ -346,6 +351,7 @@ private function processStmtNode(
346351
Node\Stmt $stmt,
347352
MutatingScope $scope,
348353
callable $nodeCallback,
354+
StatementContext $context,
349355
): StatementResult
350356
{
351357
if (
@@ -460,7 +466,7 @@ private function processStmtNode(
460466
}
461467

462468
$gatheredReturnStatements[] = new ReturnStatement($scope, $node);
463-
});
469+
}, StatementContext::createTopLevel());
464470

465471
$nodeCallback(new FunctionReturnStatementsNode(
466472
$stmt,
@@ -560,7 +566,7 @@ private function processStmtNode(
560566
}
561567

562568
$gatheredReturnStatements[] = new ReturnStatement($scope, $node);
563-
});
569+
}, StatementContext::createTopLevel());
564570
$nodeCallback(new MethodReturnStatementsNode(
565571
$stmt,
566572
$gatheredReturnStatements,
@@ -629,7 +635,7 @@ private function processStmtNode(
629635
$scope = $scope->enterNamespace($stmt->name->toString());
630636
}
631637

632-
$scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback)->getScope();
638+
$scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope();
633639
$hasYield = false;
634640
$throwPoints = [];
635641
} elseif ($stmt instanceof Node\Stmt\Trait_) {
@@ -659,7 +665,7 @@ private function processStmtNode(
659665
$classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
660666
$this->processAttributeGroups($stmt->attrGroups, $classScope, $classStatementsGatherer);
661667

662-
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer);
668+
$this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);
663669
$nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope);
664670
$nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope);
665671
$nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope);
@@ -670,7 +676,7 @@ private function processStmtNode(
670676
$this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
671677

672678
foreach ($stmt->props as $prop) {
673-
$this->processStmtNode($prop, $scope, $nodeCallback);
679+
$this->processStmtNode($prop, $scope, $nodeCallback, $context);
674680
[,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags] = $this->getPhpDocs($scope, $stmt);
675681
if (!$scope->isInClass()) {
676682
throw new ShouldNotHappenException();
@@ -726,7 +732,7 @@ private function processStmtNode(
726732
$alwaysTerminating = true;
727733
$hasYield = $condResult->hasYield();
728734

729-
$branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback);
735+
$branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
730736

731737
if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) {
732738
$exitPoints = $branchScopeStatementResult->getExitPoints();
@@ -747,7 +753,7 @@ private function processStmtNode(
747753
$condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep());
748754
$throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
749755
$condScope = $condResult->getScope();
750-
$branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback);
756+
$branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
751757

752758
if (
753759
!$ifAlwaysTrue
@@ -784,7 +790,7 @@ private function processStmtNode(
784790
}
785791
} else {
786792
$nodeCallback($stmt->else, $scope);
787-
$branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback);
793+
$branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context);
788794

789795
if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
790796
$exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
@@ -825,12 +831,15 @@ private function processStmtNode(
825831
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
826832
$bodyScope = $this->enterForeach($bodyScope, $stmt);
827833
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
828-
})->filterOutLoopExitPoints();
834+
}, $context->enterDeep())->filterOutLoopExitPoints();
829835
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
830836
$bodyScope = $bodyScopeResult->getScope();
831837
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
832838
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
833839
}
840+
if (!$context->isTopLevel()) {
841+
break;
842+
}
834843
if ($bodyScope->equals($prevScope)) {
835844
break;
836845
}
@@ -843,7 +852,7 @@ private function processStmtNode(
843852

844853
$bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
845854
$bodyScope = $this->enterForeach($bodyScope, $stmt);
846-
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints();
855+
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
847856
$finalScope = $finalScopeResult->getScope();
848857
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
849858
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
@@ -891,12 +900,15 @@ private function processStmtNode(
891900
$bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void {
892901
}, ExpressionContext::createDeep())->getTruthyScope();
893902
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
894-
})->filterOutLoopExitPoints();
903+
}, $context->enterDeep())->filterOutLoopExitPoints();
895904
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
896905
$bodyScope = $bodyScopeResult->getScope();
897906
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
898907
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
899908
}
909+
if (!$context->isTopLevel()) {
910+
break;
911+
}
900912
if ($bodyScope->equals($prevScope)) {
901913
break;
902914
}
@@ -910,7 +922,7 @@ private function processStmtNode(
910922
$bodyScope = $bodyScope->mergeWith($scope);
911923
$bodyScopeMaybeRan = $bodyScope;
912924
$bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
913-
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints();
925+
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
914926
$finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
915927
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
916928
$finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
@@ -965,7 +977,7 @@ private function processStmtNode(
965977
$prevScope = $bodyScope;
966978
$bodyScope = $bodyScope->mergeWith($scope);
967979
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
968-
})->filterOutLoopExitPoints();
980+
}, $context->enterDeep())->filterOutLoopExitPoints();
969981
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
970982
$bodyScope = $bodyScopeResult->getScope();
971983
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
@@ -977,6 +989,9 @@ private function processStmtNode(
977989
}
978990
$bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void {
979991
}, ExpressionContext::createDeep())->getTruthyScope();
992+
if (!$context->isTopLevel()) {
993+
break;
994+
}
980995
if ($bodyScope->equals($prevScope)) {
981996
break;
982997
}
@@ -989,7 +1004,7 @@ private function processStmtNode(
9891004

9901005
$bodyScope = $bodyScope->mergeWith($scope);
9911006

992-
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints();
1007+
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
9931008
$bodyScope = $bodyScopeResult->getScope();
9941009
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
9951010
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
@@ -1065,7 +1080,7 @@ private function processStmtNode(
10651080
}, ExpressionContext::createDeep())->getTruthyScope();
10661081
}
10671082
$bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1068-
})->filterOutLoopExitPoints();
1083+
}, $context->enterDeep())->filterOutLoopExitPoints();
10691084
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
10701085
$bodyScope = $bodyScopeResult->getScope();
10711086
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
@@ -1079,6 +1094,10 @@ private function processStmtNode(
10791094
$throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
10801095
}
10811096

1097+
if (!$context->isTopLevel()) {
1098+
break;
1099+
}
1100+
10821101
if ($bodyScope->equals($prevScope)) {
10831102
break;
10841103
}
@@ -1094,7 +1113,7 @@ private function processStmtNode(
10941113
$bodyScope = $this->processExprNode($condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
10951114
}
10961115

1097-
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback)->filterOutLoopExitPoints();
1116+
$finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
10981117
$finalScope = $finalScopeResult->getScope();
10991118
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
11001119
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
@@ -1164,7 +1183,7 @@ private function processStmtNode(
11641183
}
11651184

11661185
$branchScope = $branchScope->mergeWith($prevScope);
1167-
$branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback);
1186+
$branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context);
11681187
$branchScope = $branchScopeResult->getScope();
11691188
$branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
11701189
$hasYield = $hasYield || $branchFinalScopeResult->hasYield();
@@ -1208,7 +1227,7 @@ private function processStmtNode(
12081227

12091228
return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints);
12101229
} elseif ($stmt instanceof TryCatch) {
1211-
$branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback);
1230+
$branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context);
12121231
$branchScope = $branchScopeResult->getScope();
12131232
$finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
12141233

@@ -1311,7 +1330,7 @@ private function processStmtNode(
13111330
$variableName = $catchNode->var->name;
13121331
}
13131332

1314-
$catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback);
1333+
$catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context);
13151334
$catchScopeForFinally = $catchScopeResult->getScope();
13161335

13171336
$finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
@@ -1355,7 +1374,7 @@ private function processStmtNode(
13551374

13561375
if ($finallyScope !== null && $stmt->finally !== null) {
13571376
$originalFinallyScope = $finallyScope;
1358-
$finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback);
1377+
$finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context);
13591378
$alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
13601379
$hasYield = $hasYield || $finallyResult->hasYield();
13611380
$throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
@@ -1384,7 +1403,7 @@ private function processStmtNode(
13841403
$hasYield = false;
13851404
$throwPoints = [];
13861405
foreach ($stmt->uses as $use) {
1387-
$this->processStmtNode($use, $scope, $nodeCallback);
1406+
$this->processStmtNode($use, $scope, $nodeCallback, $context);
13881407
}
13891408
} elseif ($stmt instanceof Node\Stmt\Global_) {
13901409
$hasYield = false;
@@ -1412,7 +1431,7 @@ private function processStmtNode(
14121431

14131432
$vars = [];
14141433
foreach ($stmt->vars as $var) {
1415-
$scope = $this->processStmtNode($var, $scope, $nodeCallback)->getScope();
1434+
$scope = $this->processStmtNode($var, $scope, $nodeCallback, $context)->getScope();
14161435
if (!is_string($var->var->name)) {
14171436
continue;
14181437
}
@@ -2542,7 +2561,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
25422561
}
25432562
} elseif ($expr->class instanceof Class_) {
25442563
$this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name
2545-
$this->processStmtNode($expr->class, $scope, $nodeCallback);
2564+
$this->processStmtNode($expr->class, $scope, $nodeCallback, StatementContext::createTopLevel());
25462565
} else {
25472566
$className = $scope->resolveName($expr->class);
25482567
if ($this->reflectionProvider->hasClass($className)) {
@@ -3149,7 +3168,7 @@ private function processClosureNode(
31493168
$gatheredReturnStatements[] = new ReturnStatement($scope, $node);
31503169
};
31513170
if (count($byRefUses) === 0) {
3152-
$statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback);
3171+
$statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
31533172
$nodeCallback(new ClosureReturnStatementsNode(
31543173
$expr,
31553174
$gatheredReturnStatements,
@@ -3165,7 +3184,7 @@ private function processClosureNode(
31653184
$prevScope = $closureScope;
31663185

31673186
$intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void {
3168-
});
3187+
}, StatementContext::createTopLevel());
31693188
$intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
31703189
foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
31713190
$intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
@@ -3181,7 +3200,7 @@ private function processClosureNode(
31813200
$count++;
31823201
} while ($count < self::LOOP_SCOPE_ITERATIONS);
31833202

3184-
$statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback);
3203+
$statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
31853204
$nodeCallback(new ClosureReturnStatementsNode(
31863205
$expr,
31873206
$gatheredReturnStatements,
@@ -4101,7 +4120,7 @@ private function processNodesForTraitUse($node, ClassReflection $traitReflection
41014120
$methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName];
41024121
$stmts[$i] = $methodAst;
41034122
}
4104-
$this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback);
4123+
$this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback, StatementContext::createTopLevel());
41054124
return;
41064125
}
41074126
if ($node instanceof Node\Stmt\ClassLike) {

src/Analyser/StatementContext.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
class StatementContext
6+
{
7+
8+
private function __construct(
9+
private bool $isTopLevel,
10+
)
11+
{
12+
}
13+
14+
public static function createTopLevel(): self
15+
{
16+
return new self(true);
17+
}
18+
19+
public static function createDeep(): self
20+
{
21+
return new self(false);
22+
}
23+
24+
public function isTopLevel(): bool
25+
{
26+
return $this->isTopLevel;
27+
}
28+
29+
public function enterDeep(): self
30+
{
31+
if ($this->isTopLevel) {
32+
return self::createDeep();
33+
}
34+
35+
return $this;
36+
}
37+
38+
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9085,7 +9085,7 @@ public function dataGeneralizeScopeRecursiveType(): array
90859085
{
90869086
return [
90879087
[
9088-
'array{}|array{foo: array<array>}',
9088+
'array{}|array{foo?: array}',
90899089
'$data',
90909090
],
90919091
];

0 commit comments

Comments
 (0)