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
274 changes: 170 additions & 104 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,22 +278,76 @@ public function processNodes(
{
$expressionResultStorage = new ExpressionResultStorage();
$alreadyTerminated = false;
$exitPoints = [];

$stmts = [];
$stmtToNodeIndex = [];
foreach ($nodes as $i => $node) {
if (
!$node instanceof Node\Stmt
|| ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike))
) {
if (!($node instanceof Node\Stmt)) {
continue;
}

$stmtToNodeIndex[count($stmts)] = $i;
$stmts[] = $node;
}

$dummyParent = new Node\Stmt\Nop();
foreach ($stmts as $si => $node) {
if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) {
continue;
}

$nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
if ($nestedLabelNames !== null) {
$scope = $this->resolveBackwardGotoScope(
$dummyParent,
[$node],
$scope,
$expressionResultStorage,
StatementContext::createDeep(),
static fn (string $name): bool => isset($nestedLabelNames[$name]),
false,
);
}

$statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel());
$scope = $statementResult->getScope();

if ($node instanceof Node\Stmt\Label) {
$labelName = $node->name->toString();

[$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
$labelName,
$scope,
$alreadyTerminated,
$exitPoints,
);

if ($alreadyTerminated) {
continue;
}

if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) {
$scope = $this->resolveBackwardGotoScope(
$dummyParent,
array_slice($stmts, $si + 1),
$scope,
$expressionResultStorage,
StatementContext::createDeep(),
static fn (string $name): bool => $name === $labelName,
true,
);
}
}

$exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());

if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
continue;
}

$alreadyTerminated = true;
$nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true);
$nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true);
$this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback);
}

Expand All @@ -308,6 +362,93 @@ protected function processPendingFibers(ExpressionResultStorage $storage): void
{
}

/**
* @param Node\Stmt[] $bodyStmts
* @param Closure(string): bool $gotoNameMatcher
*/
private function resolveBackwardGotoScope(
Node $parentNode,
array $bodyStmts,
MutatingScope $scope,
ExpressionResultStorage $storage,
StatementContext $context,
Closure $gotoNameMatcher,
bool $mergeBodyScopeEachIteration,
): MutatingScope
{
$bodyScope = $scope;
$count = 0;
do {
$prevScope = $bodyScope;
if ($mergeBodyScopeEachIteration) {
$bodyScope = $bodyScope->mergeWith($scope);
}
$tempStorage = $storage->duplicate();
$bodyScopeResult = $this->processStmtNodesInternal(
$parentNode,
$bodyStmts,
$bodyScope,
$tempStorage,
new NoopNodeCallback(),
$context,
);

$gotoScope = null;
foreach ($bodyScopeResult->getExitPoints() as $ep) {
$epStmt = $ep->getStatement();
if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) {
continue;
}

$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
}

if ($gotoScope !== null) {
$bodyScope = $scope->mergeWith($gotoScope);
}

if ($bodyScope->equals($prevScope)) {
break;
}

if ($count >= self::GENERALIZE_AFTER_ITERATION) {
$bodyScope = $prevScope->generalizeWith($bodyScope);
}
$count++;
} while ($count < self::LOOP_SCOPE_ITERATIONS);

return $bodyScope;
}

/**
* @param InternalStatementExitPoint[] $exitPoints
* @return array{MutatingScope, bool, list<InternalStatementExitPoint>}
*/
private function mergeForwardGotoExitPoints(
string $labelName,
MutatingScope $scope,
bool $alreadyTerminated,
array $exitPoints,
): array
{
$newExitPoints = [];
foreach ($exitPoints as $exitPoint) {
$exitStmt = $exitPoint->getStatement();
if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) {
if ($alreadyTerminated) {
$scope = $exitPoint->getScope();
$alreadyTerminated = false;
} else {
$scope = $scope->mergeWith($exitPoint->getScope());
}
} else {
$newExitPoints[] = $exitPoint;
}
}

return [$scope, $alreadyTerminated, $newExitPoints];
}

/**
* @param Node\Stmt[] $nextStmts
* @param callable(Node $node, Scope $scope): void $nodeCallback
Expand Down Expand Up @@ -420,47 +561,15 @@ private function processStmtNodesInternalWithoutFlushingPendingFibers(

$nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
if ($nestedLabelNames !== null && $context->isTopLevel()) {
$originalStorage = $storage;
$bodyScope = $scope;
$count = 0;
do {
$prevScope = $bodyScope;
$tempStorage = $originalStorage->duplicate();
$bodyScopeResult = $this->processStmtNodesInternal(
$parentNode,
[$stmt],
$bodyScope,
$tempStorage,
new NoopNodeCallback(),
$context->enterDeep(),
);

$gotoScope = null;
foreach ($bodyScopeResult->getExitPoints() as $ep) {
$epStmt = $ep->getStatement();
if (!($epStmt instanceof Goto_) || !isset($nestedLabelNames[$epStmt->name->toString()])) {
continue;
}

$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
}

if ($gotoScope !== null) {
$bodyScope = $scope->mergeWith($gotoScope);
}

if ($bodyScope->equals($prevScope)) {
break;
}

if ($count >= self::GENERALIZE_AFTER_ITERATION) {
$bodyScope = $prevScope->generalizeWith($bodyScope);
}
$count++;
} while ($count < self::LOOP_SCOPE_ITERATIONS);

$scope = $bodyScope;
$storage = $originalStorage;
$scope = $this->resolveBackwardGotoScope(
$parentNode,
[$stmt],
$scope,
$storage,
$context->enterDeep(),
static fn (string $name): bool => isset($nestedLabelNames[$name]),
false,
);
}

$statementResult = $this->processStmtNode(
Expand All @@ -476,70 +585,27 @@ private function processStmtNodesInternalWithoutFlushingPendingFibers(
if ($stmt instanceof Node\Stmt\Label) {
$labelName = $stmt->name->toString();

$newExitPoints = [];
foreach ($exitPoints as $exitPoint) {
$exitStmt = $exitPoint->getStatement();
if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) {
if ($alreadyTerminated) {
$scope = $exitPoint->getScope();
$alreadyTerminated = false;
} else {
$scope = $scope->mergeWith($exitPoint->getScope());
}
} else {
$newExitPoints[] = $exitPoint;
}
}
$exitPoints = $newExitPoints;
[$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
$labelName,
$scope,
$alreadyTerminated,
$exitPoints,
);

if ($alreadyTerminated) {
continue;
}

if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) {
$originalStorage = $storage;
$bodyStmts = array_slice($stmts, $i + 1);
$bodyScope = $scope;
$count = 0;
do {
$prevScope = $bodyScope;
$bodyScope = $bodyScope->mergeWith($scope);
$tempStorage = $originalStorage->duplicate();
$bodyScopeResult = $this->processStmtNodesInternal(
$parentNode,
$bodyStmts,
$bodyScope,
$tempStorage,
new NoopNodeCallback(),
$context->enterDeep(),
);

$gotoScope = null;
foreach ($bodyScopeResult->getExitPoints() as $ep) {
$epStmt = $ep->getStatement();
if (!($epStmt instanceof Goto_) || $epStmt->name->toString() !== $labelName) {
continue;
}

$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
}

if ($gotoScope !== null) {
$bodyScope = $scope->mergeWith($gotoScope);
}

if ($bodyScope->equals($prevScope)) {
break;
}

if ($count >= self::GENERALIZE_AFTER_ITERATION) {
$bodyScope = $prevScope->generalizeWith($bodyScope);
}
$count++;
} while ($count < self::LOOP_SCOPE_ITERATIONS);

$scope = $bodyScope;
$storage = $originalStorage;
$scope = $this->resolveBackwardGotoScope(
$parentNode,
array_slice($stmts, $i + 1),
$scope,
$storage,
$context->enterDeep(),
static fn (string $name): bool => $name === $labelName,
true,
);
}
}

Expand Down
9 changes: 9 additions & 0 deletions src/Parser/GotoLabelVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public function beforeTraverse(array $nodes): ?array
#[Override]
public function afterTraverse(array $nodes): ?array
{
$stmts = [];
foreach ($nodes as $node) {
if (!($node instanceof Node\Stmt)) {
continue;
}

$stmts[] = $node;
}
$this->processStatementList($stmts);
$this->popScope();
$this->subtreeData = [];
return null;
Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14660-top-level.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

use function PHPStan\Testing\assertType;

// top-level forward goto
$id = null;
if (random_int(0, 1))
goto fin;
$id = 1;
fin:
assertType('1|null', $id);

// top-level backward goto
$ok = false;
retry:
assertType('bool', $ok);
if (!$ok) {
$ok = (bool) random_int(0, 1);
goto retry;
}
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14660.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace Bug14660;

use function PHPStan\Testing\assertType;

function test_with_forward_goto(): void {
$id = null;
if (random_int(0, 1))
goto fin;
$id = 1;
fin:
assertType('1|null', $id);
}

function test_with_backward_goto(): void {
$ok = false;
retry:
assertType('bool', $ok);
if (!$ok) {
$ok = (bool) random_int(0, 1);
goto retry;
}
}
Loading