Skip to content
Open
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
136 changes: 135 additions & 1 deletion src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use PhpParser\Node\Stmt\Echo_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\InlineHTML;
use PhpParser\Node\Stmt\Return_;
Expand Down Expand Up @@ -110,6 +111,7 @@
use PHPStan\Node\VarTagChangedExpressionTypeNode;
use PHPStan\Parser\ArrowFunctionArgVisitor;
use PHPStan\Parser\ClosureArgVisitor;
use PHPStan\Parser\GotoLabelVisitor;
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
use PHPStan\Parser\LineAttributesVisitor;
use PHPStan\Parser\Parser;
Expand Down Expand Up @@ -408,12 +410,59 @@ private function processStmtNodesInternalWithoutFlushingPendingFibers(
|| $parentNode instanceof Node\Stmt\ClassMethod
|| $parentNode instanceof PropertyHookStatementNode
|| $parentNode instanceof Expr\Closure;

foreach ($stmts as $i => $stmt) {
if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) {
if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) {
continue;
}

$isLast = $i === $stmtCount - 1;

$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;
}

$statementResult = $this->processStmtNode(
$stmt,
$scope,
Expand All @@ -424,6 +473,76 @@ private function processStmtNodesInternalWithoutFlushingPendingFibers(
$scope = $statementResult->getScope();
$hasYield = $hasYield || $statementResult->hasYield();

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;

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;
}
}

if ($shouldCheckLastStatement && $isLast) {
$endStatements = $statementResult->getEndStatements();
if (count($endStatements) > 0) {
Expand Down Expand Up @@ -917,6 +1036,18 @@ public function processStmtNode(
return new InternalStatementResult($scope, $hasYield, true, [
new InternalStatementExitPoint($stmt, $scope),
], $overridingThrowPoints ?? $throwPoints, $impurePoints);
} elseif ($stmt instanceof Goto_) {
$hasYield = false;
$throwPoints = [];
$impurePoints = [];

return new InternalStatementResult($scope, $hasYield, true, [
new InternalStatementExitPoint($stmt, $scope),
], $overridingThrowPoints ?? $throwPoints, $impurePoints);
} elseif ($stmt instanceof Node\Stmt\Label) {
$hasYield = false;
$throwPoints = $overridingThrowPoints ?? [];
$impurePoints = [];
} elseif ($stmt instanceof Node\Stmt\Expression) {
if ($stmt->expr instanceof Expr\Throw_) {
$scope = $stmtScope;
Expand Down Expand Up @@ -4800,6 +4931,9 @@ private function getNextUnreachableStatements(array $nodes, bool $earlyBinding):
$stmts = [];
$isPassedUnreachableStatement = false;
foreach ($nodes as $node) {
if ($node instanceof Node\Stmt\Label) {
break;
}
if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
continue;
}
Expand Down
177 changes: 177 additions & 0 deletions src/Parser/GotoLabelVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
<?php declare(strict_types = 1);

namespace PHPStan\Parser;

use Override;
use PhpParser\Node;
use PhpParser\Node\Stmt\Goto_;
use PhpParser\NodeVisitorAbstract;
use PHPStan\DependencyInjection\AutowiredService;
use function array_slice;
use function is_array;

#[AutowiredService]
final class GotoLabelVisitor extends NodeVisitorAbstract
{

public const HAS_BACKWARD_GOTO_ATTRIBUTE = 'hasBackwardGoto';

public const NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE = 'nestedBackwardGotoLabels';

#[Override]
public function beforeTraverse(array $nodes): ?array
{
$this->processStatementList($nodes);
return null;
}

#[Override]
public function enterNode(Node $node): ?Node
{
if ($node instanceof Node\PropertyHook) {
if (is_array($node->body)) {
$this->processStatementList($node->body);
}
return null;
}

if (
$node instanceof Node\Stmt\Function_
|| $node instanceof Node\Stmt\ClassMethod
|| $node instanceof Node\Expr\Closure
|| $node instanceof Node\Stmt\If_
|| $node instanceof Node\Stmt\ElseIf_
|| $node instanceof Node\Stmt\Else_
|| $node instanceof Node\Stmt\Case_
|| $node instanceof Node\Stmt\Catch_
|| $node instanceof Node\Stmt\Do_
|| $node instanceof Node\Stmt\Finally_
|| $node instanceof Node\Stmt\For_
|| $node instanceof Node\Stmt\Foreach_
|| $node instanceof Node\Stmt\Namespace_
|| $node instanceof Node\Stmt\TryCatch
|| $node instanceof Node\Stmt\While_
|| $node instanceof Node\Stmt\Block
|| $node instanceof Node\Stmt\Declare_
) {
$stmts = $node->stmts ?? [];
if ($stmts !== []) {
$this->processStatementList($stmts);
}
}

return null;
}

/**
* @param array<Node> $stmts
*/
private function processStatementList(array $stmts): void
{
$labelIndices = [];
foreach ($stmts as $idx => $s) {
if (!($s instanceof Node\Stmt\Label)) {
continue;
}

$labelIndices[$s->name->toString()] = $idx;
}

if ($labelIndices !== []) {
foreach ($labelIndices as $labelName => $labelIdx) {
$stmtsAfterLabel = array_slice($stmts, $labelIdx + 1);
if ($stmtsAfterLabel === []) {
continue;
}
/** @var Goto_[] $gotos */
$gotos = $this->findWithinScope($stmtsAfterLabel, Goto_::class);
foreach ($gotos as $goto) {
if ($goto->name->toString() === $labelName) {
$stmts[$labelIdx]->setAttribute(self::HAS_BACKWARD_GOTO_ATTRIBUTE, true);
break;
}
}
}
}

foreach ($stmts as $s) {
if ($s instanceof Node\Stmt\Label) {
continue;
}
/** @var Node\Stmt\Label[] $nestedLabels */
$nestedLabels = $this->findWithinScope([$s], Node\Stmt\Label::class);
if ($nestedLabels === []) {
continue;
}
/** @var Goto_[] $nestedGotos */
$nestedGotos = $this->findWithinScope([$s], Goto_::class);
if ($nestedGotos === []) {
continue;
}
$nestedLabelNames = [];
foreach ($nestedLabels as $nestedLabel) {
$nestedLabelNames[$nestedLabel->name->toString()] = true;
}
$matchedLabels = [];
foreach ($nestedGotos as $nestedGoto) {
$gotoName = $nestedGoto->name->toString();
if (!isset($nestedLabelNames[$gotoName])) {
continue;
}
$matchedLabels[$gotoName] = true;
}
if ($matchedLabels === []) {
continue;
}

$s->setAttribute(self::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE, $matchedLabels);
}
}

/**
* @param array<mixed> $nodes
* @param class-string<Node> $class
* @return Node[]
*/
private function findWithinScope(array $nodes, string $class): array
{
$results = [];
$this->collectWithinScope($nodes, $class, $results);
return $results;
}

/**
* @param array<mixed> $nodes
* @param class-string<Node> $class
* @param Node[] $results
*/
private function collectWithinScope(array $nodes, string $class, array &$results): void
{
foreach ($nodes as $node) {
if (!$node instanceof Node) {
continue;
}
if ($node instanceof $class) {
$results[] = $node;
}
if (
$node instanceof Node\Stmt\Function_
|| $node instanceof Node\Stmt\ClassMethod
|| $node instanceof Node\Expr\Closure
|| $node instanceof Node\Stmt\ClassLike
|| $node instanceof Node\PropertyHook
) {
continue;
}
foreach ($node->getSubNodeNames() as $name) {
$sub = $node->{$name};
if ($sub instanceof Node) {
$this->collectWithinScope([$sub], $class, $results);
} elseif (is_array($sub)) {
$this->collectWithinScope($sub, $class, $results);
}
}
}
}

}
34 changes: 34 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-4674.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types = 1);

namespace Bug4674;

use function PHPStan\Testing\assertType;

/**
* @return string|false
*/
function string_or_false()
{
if (rand(1,2)==1)
return "string";
return false;
}

function takes_string(string $s): void
{
echo $s;
}

function test(): void
{
$a = string_or_false();
if ($a === false)
goto end;

assertType('string', $a);
takes_string($a);

end:
assertType('string|false', $a);
echo "finished";
}
Loading
Loading