Skip to content

Commit f0931f5

Browse files
authored
Handle goto and Label in top-level file statements processed by processNodes (#5721)
1 parent 5659924 commit f0931f5

4 files changed

Lines changed: 223 additions & 104 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 170 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -278,22 +278,76 @@ public function processNodes(
278278
{
279279
$expressionResultStorage = new ExpressionResultStorage();
280280
$alreadyTerminated = false;
281+
$exitPoints = [];
282+
283+
$stmts = [];
284+
$stmtToNodeIndex = [];
281285
foreach ($nodes as $i => $node) {
282-
if (
283-
!$node instanceof Node\Stmt
284-
|| ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike))
285-
) {
286+
if (!($node instanceof Node\Stmt)) {
286287
continue;
287288
}
288289

290+
$stmtToNodeIndex[count($stmts)] = $i;
291+
$stmts[] = $node;
292+
}
293+
294+
$dummyParent = new Node\Stmt\Nop();
295+
foreach ($stmts as $si => $node) {
296+
if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) {
297+
continue;
298+
}
299+
300+
$nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
301+
if ($nestedLabelNames !== null) {
302+
$scope = $this->resolveBackwardGotoScope(
303+
$dummyParent,
304+
[$node],
305+
$scope,
306+
$expressionResultStorage,
307+
StatementContext::createDeep(),
308+
static fn (string $name): bool => isset($nestedLabelNames[$name]),
309+
false,
310+
);
311+
}
312+
289313
$statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel());
290314
$scope = $statementResult->getScope();
315+
316+
if ($node instanceof Node\Stmt\Label) {
317+
$labelName = $node->name->toString();
318+
319+
[$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
320+
$labelName,
321+
$scope,
322+
$alreadyTerminated,
323+
$exitPoints,
324+
);
325+
326+
if ($alreadyTerminated) {
327+
continue;
328+
}
329+
330+
if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) {
331+
$scope = $this->resolveBackwardGotoScope(
332+
$dummyParent,
333+
array_slice($stmts, $si + 1),
334+
$scope,
335+
$expressionResultStorage,
336+
StatementContext::createDeep(),
337+
static fn (string $name): bool => $name === $labelName,
338+
true,
339+
);
340+
}
341+
}
342+
343+
$exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
344+
291345
if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
292346
continue;
293347
}
294348

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

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

365+
/**
366+
* @param Node\Stmt[] $bodyStmts
367+
* @param Closure(string): bool $gotoNameMatcher
368+
*/
369+
private function resolveBackwardGotoScope(
370+
Node $parentNode,
371+
array $bodyStmts,
372+
MutatingScope $scope,
373+
ExpressionResultStorage $storage,
374+
StatementContext $context,
375+
Closure $gotoNameMatcher,
376+
bool $mergeBodyScopeEachIteration,
377+
): MutatingScope
378+
{
379+
$bodyScope = $scope;
380+
$count = 0;
381+
do {
382+
$prevScope = $bodyScope;
383+
if ($mergeBodyScopeEachIteration) {
384+
$bodyScope = $bodyScope->mergeWith($scope);
385+
}
386+
$tempStorage = $storage->duplicate();
387+
$bodyScopeResult = $this->processStmtNodesInternal(
388+
$parentNode,
389+
$bodyStmts,
390+
$bodyScope,
391+
$tempStorage,
392+
new NoopNodeCallback(),
393+
$context,
394+
);
395+
396+
$gotoScope = null;
397+
foreach ($bodyScopeResult->getExitPoints() as $ep) {
398+
$epStmt = $ep->getStatement();
399+
if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) {
400+
continue;
401+
}
402+
403+
$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
404+
}
405+
406+
if ($gotoScope !== null) {
407+
$bodyScope = $scope->mergeWith($gotoScope);
408+
}
409+
410+
if ($bodyScope->equals($prevScope)) {
411+
break;
412+
}
413+
414+
if ($count >= self::GENERALIZE_AFTER_ITERATION) {
415+
$bodyScope = $prevScope->generalizeWith($bodyScope);
416+
}
417+
$count++;
418+
} while ($count < self::LOOP_SCOPE_ITERATIONS);
419+
420+
return $bodyScope;
421+
}
422+
423+
/**
424+
* @param InternalStatementExitPoint[] $exitPoints
425+
* @return array{MutatingScope, bool, list<InternalStatementExitPoint>}
426+
*/
427+
private function mergeForwardGotoExitPoints(
428+
string $labelName,
429+
MutatingScope $scope,
430+
bool $alreadyTerminated,
431+
array $exitPoints,
432+
): array
433+
{
434+
$newExitPoints = [];
435+
foreach ($exitPoints as $exitPoint) {
436+
$exitStmt = $exitPoint->getStatement();
437+
if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) {
438+
if ($alreadyTerminated) {
439+
$scope = $exitPoint->getScope();
440+
$alreadyTerminated = false;
441+
} else {
442+
$scope = $scope->mergeWith($exitPoint->getScope());
443+
}
444+
} else {
445+
$newExitPoints[] = $exitPoint;
446+
}
447+
}
448+
449+
return [$scope, $alreadyTerminated, $newExitPoints];
450+
}
451+
311452
/**
312453
* @param Node\Stmt[] $nextStmts
313454
* @param callable(Node $node, Scope $scope): void $nodeCallback
@@ -420,47 +561,15 @@ private function processStmtNodesInternalWithoutFlushingPendingFibers(
420561

421562
$nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
422563
if ($nestedLabelNames !== null && $context->isTopLevel()) {
423-
$originalStorage = $storage;
424-
$bodyScope = $scope;
425-
$count = 0;
426-
do {
427-
$prevScope = $bodyScope;
428-
$tempStorage = $originalStorage->duplicate();
429-
$bodyScopeResult = $this->processStmtNodesInternal(
430-
$parentNode,
431-
[$stmt],
432-
$bodyScope,
433-
$tempStorage,
434-
new NoopNodeCallback(),
435-
$context->enterDeep(),
436-
);
437-
438-
$gotoScope = null;
439-
foreach ($bodyScopeResult->getExitPoints() as $ep) {
440-
$epStmt = $ep->getStatement();
441-
if (!($epStmt instanceof Goto_) || !isset($nestedLabelNames[$epStmt->name->toString()])) {
442-
continue;
443-
}
444-
445-
$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
446-
}
447-
448-
if ($gotoScope !== null) {
449-
$bodyScope = $scope->mergeWith($gotoScope);
450-
}
451-
452-
if ($bodyScope->equals($prevScope)) {
453-
break;
454-
}
455-
456-
if ($count >= self::GENERALIZE_AFTER_ITERATION) {
457-
$bodyScope = $prevScope->generalizeWith($bodyScope);
458-
}
459-
$count++;
460-
} while ($count < self::LOOP_SCOPE_ITERATIONS);
461-
462-
$scope = $bodyScope;
463-
$storage = $originalStorage;
564+
$scope = $this->resolveBackwardGotoScope(
565+
$parentNode,
566+
[$stmt],
567+
$scope,
568+
$storage,
569+
$context->enterDeep(),
570+
static fn (string $name): bool => isset($nestedLabelNames[$name]),
571+
false,
572+
);
464573
}
465574

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

479-
$newExitPoints = [];
480-
foreach ($exitPoints as $exitPoint) {
481-
$exitStmt = $exitPoint->getStatement();
482-
if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) {
483-
if ($alreadyTerminated) {
484-
$scope = $exitPoint->getScope();
485-
$alreadyTerminated = false;
486-
} else {
487-
$scope = $scope->mergeWith($exitPoint->getScope());
488-
}
489-
} else {
490-
$newExitPoints[] = $exitPoint;
491-
}
492-
}
493-
$exitPoints = $newExitPoints;
588+
[$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
589+
$labelName,
590+
$scope,
591+
$alreadyTerminated,
592+
$exitPoints,
593+
);
494594

495595
if ($alreadyTerminated) {
496596
continue;
497597
}
498598

499599
if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) {
500-
$originalStorage = $storage;
501-
$bodyStmts = array_slice($stmts, $i + 1);
502-
$bodyScope = $scope;
503-
$count = 0;
504-
do {
505-
$prevScope = $bodyScope;
506-
$bodyScope = $bodyScope->mergeWith($scope);
507-
$tempStorage = $originalStorage->duplicate();
508-
$bodyScopeResult = $this->processStmtNodesInternal(
509-
$parentNode,
510-
$bodyStmts,
511-
$bodyScope,
512-
$tempStorage,
513-
new NoopNodeCallback(),
514-
$context->enterDeep(),
515-
);
516-
517-
$gotoScope = null;
518-
foreach ($bodyScopeResult->getExitPoints() as $ep) {
519-
$epStmt = $ep->getStatement();
520-
if (!($epStmt instanceof Goto_) || $epStmt->name->toString() !== $labelName) {
521-
continue;
522-
}
523-
524-
$gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
525-
}
526-
527-
if ($gotoScope !== null) {
528-
$bodyScope = $scope->mergeWith($gotoScope);
529-
}
530-
531-
if ($bodyScope->equals($prevScope)) {
532-
break;
533-
}
534-
535-
if ($count >= self::GENERALIZE_AFTER_ITERATION) {
536-
$bodyScope = $prevScope->generalizeWith($bodyScope);
537-
}
538-
$count++;
539-
} while ($count < self::LOOP_SCOPE_ITERATIONS);
540-
541-
$scope = $bodyScope;
542-
$storage = $originalStorage;
600+
$scope = $this->resolveBackwardGotoScope(
601+
$parentNode,
602+
array_slice($stmts, $i + 1),
603+
$scope,
604+
$storage,
605+
$context->enterDeep(),
606+
static fn (string $name): bool => $name === $labelName,
607+
true,
608+
);
543609
}
544610
}
545611

src/Parser/GotoLabelVisitor.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ public function beforeTraverse(array $nodes): ?array
4646
#[Override]
4747
public function afterTraverse(array $nodes): ?array
4848
{
49+
$stmts = [];
50+
foreach ($nodes as $node) {
51+
if (!($node instanceof Node\Stmt)) {
52+
continue;
53+
}
54+
55+
$stmts[] = $node;
56+
}
57+
$this->processStatementList($stmts);
4958
$this->popScope();
5059
$this->subtreeData = [];
5160
return null;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
use function PHPStan\Testing\assertType;
4+
5+
// top-level forward goto
6+
$id = null;
7+
if (random_int(0, 1))
8+
goto fin;
9+
$id = 1;
10+
fin:
11+
assertType('1|null', $id);
12+
13+
// top-level backward goto
14+
$ok = false;
15+
retry:
16+
assertType('bool', $ok);
17+
if (!$ok) {
18+
$ok = (bool) random_int(0, 1);
19+
goto retry;
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14660;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function test_with_forward_goto(): void {
8+
$id = null;
9+
if (random_int(0, 1))
10+
goto fin;
11+
$id = 1;
12+
fin:
13+
assertType('1|null', $id);
14+
}
15+
16+
function test_with_backward_goto(): void {
17+
$ok = false;
18+
retry:
19+
assertType('bool', $ok);
20+
if (!$ok) {
21+
$ok = (bool) random_int(0, 1);
22+
goto retry;
23+
}
24+
}

0 commit comments

Comments
 (0)