Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions src/Analyser/InternalStatementResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
final class InternalStatementResult
{

private bool $endReachable;

/**
* @param InternalStatementExitPoint[] $exitPoints
* @param InternalThrowPoint[] $throwPoints
Expand All @@ -24,8 +26,10 @@ public function __construct(
private array $impurePoints,
private array $endStatements = [],
private ?VariableFlow $variableFlow = null,
?bool $endReachable = null,
)
{
$this->endReachable = $endReachable ?? !$isAlwaysTerminating;
foreach ($exitPoints as $exitPoint) {
$this->scope = $this->scope->addTemplateArgumentConstraints($exitPoint->getScope()->getTemplateArgumentConstraints());
}
Expand All @@ -39,6 +43,30 @@ public function getVariableFlow(): ?VariableFlow
return $this->variableFlow;
}

/**
* Whether execution can reach the end of the statements. Unlike isAlwaysTerminating(),
* filterOutLoopExitPoints() does not reset it: a loop body left only through
* break or continue still cannot reach its end.
*/
public function isEndReachable(): bool
{
return $this->endReachable;
}

/**
* The scope the next iteration of a loop starts from: the end of the body when
* it is reachable, merged with the body's continue statements. Null when the body
* never reaches the next iteration.
*/
public function getLoopBackEdgeScope(): ?MutatingScope
{
$backEdge = $this->endReachable ? $this->scope : null;
foreach ($this->getExitPointsByType(Stmt\Continue_::class) as $continueExitPoint) {
$backEdge = $backEdge === null ? $continueExitPoint->getScope() : $backEdge->mergeWith($continueExitPoint->getScope());
}
return $backEdge;
}

public function toPublic(): StatementResult
{
return new StatementResult(
Expand Down Expand Up @@ -81,14 +109,14 @@ public function filterOutLoopExitPoints(): self

$num = $statement->num;
if (!$num instanceof Int_) {
return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow);
return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow, endReachable: false);
}

if ($num->value !== 1) {
continue;
}

return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow);
return new self($this->scope, $this->hasYield, false, $this->exitPoints, $this->throwPoints, $this->impurePoints, variableFlow: $this->variableFlow, endReachable: false);
}

return $this;
Expand Down
32 changes: 16 additions & 16 deletions src/Analyser/StmtHandler/DoWhileHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\Do_;
use PHPStan\Analyser\ExpressionContext;
use PHPStan\Analyser\ExpressionResultStorage;
Expand Down Expand Up @@ -72,12 +71,8 @@ public function processStmt(
$scope->pushExpressionResultStorage($storage);
try {
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
$bodyScope = $bodyScopeResult->getScope();
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
}
$finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
$finalScope = $backEdgeScope === null ? $finalScope : $backEdgeScope->mergeWith($finalScope);
foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
}
Expand All @@ -88,10 +83,16 @@ public function processStmt(
$replayPassStorage = $storage;
$replayPassResult = $bodyScopeResult;
}
$bodyScope = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope();
if ($backEdgeScope !== null) {
$bodyScope = $nodeScopeResolver->processExprNode($stmt, $stmt->cond, $backEdgeScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope();
}
} finally {
$scope->popExpressionResultStorage();
}
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
}
if ($bodyScope->equals($prevScope)) {
break;
}
Expand Down Expand Up @@ -120,10 +121,9 @@ public function processStmt(
} else {
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
}
$bodyScope = $bodyScopeResult->getScope();
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
}
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
$backEdgeDead = $backEdgeScope === null;
$bodyScope = $backEdgeScope ?? $bodyScopeResult->getScope();

// the condition is processed once on the post-body scope; its result
// answers both the always-iterates check below and the falsey post-loop
Expand All @@ -138,16 +138,16 @@ public function processStmt(
$alwaysIterates = $condBooleanType->isTrue()->yes();
}

if ($alwaysIterates) {
if ($alwaysIterates || $backEdgeDead) {
$alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
} else {
$alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
}
$finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
$finalScope = $alwaysTerminating || $backEdgeDead ? $finalScope : $bodyScope->mergeWith($finalScope);
if ($finalScope === null) {
$finalScope = $scope;
}
if (!$alwaysTerminating) {
if (!$alwaysTerminating && !$backEdgeDead) {
$hasYield = $condResult->hasYield();
$throwPoints = $condResult->getThrowPoints();
$impurePoints = $condResult->getImpurePoints();
Expand All @@ -162,7 +162,7 @@ public function processStmt(

$breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class);
if (count($breakExitPoints) > 0) {
$breakScope = $alwaysIterates ? null : $finalScope;
$breakScope = $alwaysIterates || $backEdgeDead ? null : $finalScope;
foreach ($breakExitPoints as $breakExitPoint) {
$breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
}
Expand Down
20 changes: 10 additions & 10 deletions src/Analyser/StmtHandler/ForHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Break_;
use PhpParser\Node\Stmt\Continue_;
use PhpParser\Node\Stmt\For_;
use PHPStan\Analyser\ExpressionContext;
use PHPStan\Analyser\ExpressionResultStorage;
Expand Down Expand Up @@ -200,10 +199,12 @@ public function processStmt(
$bodyScope = $nodeScopeResolver->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getTruthyScope();
}
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$bodyScope = $bodyScopeResult->getScope();
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
}
$bodyScope = $backEdgeScope;

foreach ($stmt->loop as $loopExpr) {
$exprResult = $nodeScopeResolver->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel(resolveTemplateArguments: false));
Expand Down Expand Up @@ -244,10 +245,9 @@ public function processStmt(
}

$finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
$finalScope = $finalScopeResult->getScope();
foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
}
$backEdgeScope = $finalScopeResult->getLoopBackEdgeScope();
$backEdgeDead = $backEdgeScope === null;
$finalScope = $backEdgeScope ?? $finalScopeResult->getScope();

$loopScope = $finalScope;
foreach ($stmt->loop as $loopExpr) {
Expand All @@ -271,14 +271,14 @@ public function processStmt(

$breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
if (count($breakExitPoints) > 0) {
$breakScope = $alwaysIterates->yes() ? null : $finalScope;
$breakScope = $alwaysIterates->yes() || $backEdgeDead ? null : $finalScope;
foreach ($breakExitPoints as $breakExitPoint) {
$breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
}
$finalScope = $breakScope;
}

if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating() || ($backEdgeDead && count($breakExitPoints) === 0)) {
if ($nodeScopeResolver->shouldPolluteScopeWithLoopInitialAssignments()) {
$finalScope = $initScope;
} else {
Expand Down
62 changes: 40 additions & 22 deletions src/Analyser/StmtHandler/ForeachHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,10 +232,12 @@ static function () use ($condResult, $emptyArrayType): Type {
try {
$bodyScope = $this->enterForeach($nodeScopeResolver, $bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback);
$bodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $bodyRecording, $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$bodyScope = $bodyScopeResult->getScope();
foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
$backEdgeScope = $bodyScopeResult->getLoopBackEdgeScope();
if ($backEdgeScope === null) {
$bodyScope = $prevScope;
break;
}
$bodyScope = $backEdgeScope;
} finally {
$scope->popExpressionResultStorage();
}
Expand Down Expand Up @@ -278,7 +280,7 @@ static function () use ($condResult, $emptyArrayType): Type {
$finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context;
$finalScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints();
}
$finalScope = $finalScopeResult->getScope();
$finalScope = $finalScopeResult->isEndReachable() ? $finalScopeResult->getScope() : null;
$scopesWithIterableValueType = [];

$keyVarExpr = null;
Expand All @@ -299,7 +301,7 @@ static function () use ($condResult, $emptyArrayType): Type {
$trackingExpr = $originalKeyVarExpr ?? $originalValueExpr;

$continueExitPointHasUnoriginalKeyType = false;
if ($trackingExpr !== null) {
if ($trackingExpr !== null && $finalScope !== null) {
if ($finalScope->hasExpressionType($trackingExpr)->yes()) {
$scopesWithIterableValueType[] = $finalScope;
} else {
Expand All @@ -309,7 +311,7 @@ static function () use ($condResult, $emptyArrayType): Type {

foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$continueScope = $continueExitPoint->getScope();
$finalScope = $continueScope->mergeWith($finalScope);
$finalScope = $finalScope === null ? $continueScope : $continueScope->mergeWith($finalScope);
if ($trackingExpr === null || !$continueScope->hasExpressionType($trackingExpr)->yes()) {
$continueExitPointHasUnoriginalKeyType = true;
continue;
Expand All @@ -318,8 +320,9 @@ static function () use ($condResult, $emptyArrayType): Type {
}
$breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
foreach ($breakExitPoints as $breakExitPoint) {
$finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
$finalScope = $finalScope === null ? $breakExitPoint->getScope() : $breakExitPoint->getScope()->mergeWith($finalScope);
}
$finalScope ??= $finalScopeResult->getScope();

if ($unrolledEndScope !== null) {
$finalScope = $unrolledEndScope;
Expand Down Expand Up @@ -628,7 +631,7 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop
}

/**
* @return array{bodyScope: MutatingScope, endScope: MutatingScope, totalKeys: int}|null
* @return array{bodyScope: MutatingScope, endScope: MutatingScope|null, totalKeys: int}|null
*/
private function tryProcessUnrolledConstantArrayForeach(
NodeScopeResolver $nodeScopeResolver,
Expand Down Expand Up @@ -755,14 +758,22 @@ private function tryProcessUnrolledConstantArrayForeach(
$bodyContext,
)->filterOutLoopExitPoints();

$iterEndScope = $bodyResult->getScope();
foreach ($bodyResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$iterEndScope = $iterEndScope->mergeWith($continueExitPoint->getScope());
}
$iterEndScope = $bodyResult->getLoopBackEdgeScope();
foreach ($bodyResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$allBreakScopes[] = $breakExitPoint->getScope();
}

if ($iterEndScope === null) {
if ($isOptional) {
// the key may be missing, the next iteration then starts from the previous one
continue;
}

// no later iteration runs, the loop is left only through its break statements
$chainScope = null;
break;
}

if ($isOptional) {
$chainScope = $iterEndScope->mergeWith($chainScope);
} else {
Expand All @@ -774,11 +785,15 @@ private function tryProcessUnrolledConstantArrayForeach(
for ($i = 1, $c = count($entryScopes); $i < $c; $i++) {
$arrayBodyScope = $arrayBodyScope->mergeWith($entryScopes[$i]);
}
if (count($entryScopes) === 1) {
if (count($entryScopes) === 1 && $chainScope !== null) {
$arrayBodyScope = $arrayBodyScope->mergeWith($chainScope);
}

$allBodyScopes[] = $arrayBodyScope;
if ($chainScope === null) {
continue;
}

$allChainScopes[] = $chainScope;
}

Expand All @@ -791,21 +806,22 @@ private function tryProcessUnrolledConstantArrayForeach(
$bodyScope = $bodyScope->mergeWith($allBodyScopes[$i]);
}

$endScope = $allChainScopes[0];
for ($i = 1, $c = count($allChainScopes); $i < $c; $i++) {
$endScope = $endScope->mergeWith($allChainScopes[$i]);
$chainEndScope = null;
foreach ($allChainScopes as $chainScope) {
$chainEndScope = $chainEndScope === null ? $chainScope : $chainEndScope->mergeWith($chainScope);
}

$endScope = $chainEndScope;
foreach ($allBreakScopes as $breakScope) {
$endScope = $endScope->mergeWith($breakScope);
$endScope = $endScope === null ? $breakScope : $endScope->mergeWith($breakScope);
}

// Unsealed shapes describe zero-or-more additional entries beyond the
// explicit keys. Run the scope-generalizing loop on top of the
// unrolled explicit iterations so body-scope variables (e.g. counters)
// account for the extra iterations while keeping the lower bound
// established by the non-optional explicit keys.
if ($hasUnsealed) {
if ($hasUnsealed && $chainEndScope !== null && $endScope !== null) {
$loopScope = $endScope;
$count = 0;
do {
Expand All @@ -814,13 +830,15 @@ private function tryProcessUnrolledConstantArrayForeach(
$iterBodyScope = $loopScope->mergeWith($endScope);
$iterBodyScope = $this->enterForeach($nodeScopeResolver, $iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback());
$iterBodyScopeResult = $nodeScopeResolver->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution())->filterOutLoopExitPoints();
$loopScope = $iterBodyScopeResult->getScope();
foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$loopScope = $loopScope->mergeWith($continueExitPoint->getScope());
}
$backEdgeScope = $iterBodyScopeResult->getLoopBackEdgeScope();
foreach ($iterBodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
$endScope = $endScope->mergeWith($breakExitPoint->getScope());
}
if ($backEdgeScope === null) {
$loopScope = $prevLoopScope;
break;
}
$loopScope = $backEdgeScope;
$bodyScope = $bodyScope->mergeWith($loopScope);
if ($loopScope->equals($prevLoopScope)) {
break;
Expand Down
Loading
Loading