Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix statement analysis after early-terminating statements #3227

Merged
merged 3 commits into from
Jul 13, 2024
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
24 changes: 16 additions & 8 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -292,24 +292,28 @@ public function processNodes(
callable $nodeCallback,
): void
{
$alreadyTerminated = false;
foreach ($nodes as $i => $node) {
if (!$node instanceof Node\Stmt) {
if (
!$node instanceof Node\Stmt
|| ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike))
) {
continue;
}

$statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel());
$scope = $statementResult->getScope();
if (!$statementResult->isAlwaysTerminating()) {
if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
continue;
}

$alreadyTerminated = true;
$nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true);
if (!$nextStmt instanceof Node\Stmt) {
continue;
}

$nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
break;
}
}

Expand Down Expand Up @@ -339,6 +343,10 @@ public function processStmtNodes(
|| $parentNode instanceof Node\Stmt\ClassMethod
|| $parentNode instanceof Expr\Closure;
foreach ($stmts as $i => $stmt) {
if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) {
continue;
}

$isLast = $i === $stmtCount - 1;
$statementResult = $this->processStmtNode(
$stmt,
Expand Down Expand Up @@ -370,16 +378,16 @@ public function processStmtNodes(
$throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints());

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

$alreadyTerminated = true;
$nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
if ($nextStmt !== null) {
$nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
if ($nextStmt === null) {
continue;
}
break;
$nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't understand these changes. So here we're going to invoke UnreachableStatementNode but then we're still going to analyse the next statement if it's not Function_ or ClassLike? I don't get it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this code:

<?php
namespace Foo;

doSomething();
exit();
foo();
bar();

function doSomething(): void
{
  // ...
}

Here foo(); will be reported as an unreachable statement.
Before this PR, PHPStan stops analyzing statements after foo() because analyzing bar() is no longer useful.
However, we still need to analyze the body of doSomething() actually.

So PHPStan should continue analysis after an unreachable statement, but the target is different;

  • Before unreachable statements ($alreadyTerminated = false)
    • analyze all statements
  • After unreachable statements ($alreadyTerminated = true)
    • analyze only functions and class-likes

Copy link
Contributor

@staabm staabm Jul 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about methods, closures, arrow-functions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@staabm 1) I don't think you can put exit in-between methods in a class
2) I don't think closures after exit are executed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@takaram Thank you, makes total sense!

}

$statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
Expand Down Expand Up @@ -6064,7 +6072,7 @@ private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Nod
if ($node instanceof Node\Stmt\Nop) {
continue;
}
if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) {
if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
continue;
}
return $node;
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,4 +218,10 @@ public function testBug8966(): void
]);
}

public function testBug11179(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/bug-11179.php'], []);
}

}
14 changes: 14 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-11179.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php declare(strict_types = 1);

namespace Bug11179;

exit(0);

function foo(string $p): string
{
\PHPStan\dumpType($p);
return "";
}

__halt_compiler();
foo
20 changes: 20 additions & 0 deletions tests/PHPStan/Rules/Debug/DumpTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,24 @@ public function testBug10377(): void
]);
}

public function testBug11179(): void
{
$this->analyse([__DIR__ . '/../DeadCode/data/bug-11179.php'], [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a test into the "Unreachable statement - code above always terminates." rule.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's in UnreachableStatementRuleTest

[
'Dumped type: string',
9,
],
]);
}

public function testBug11179NoNamespace(): void
{
$this->analyse([__DIR__ . '/data/bug-11179-no-namespace.php'], [
[
'Dumped type: string',
11,
],
]);
}

}
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Debug/data/bug-11179-no-namespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php declare(strict_types = 1);

// no namespace

exit(0);

echo 1;

function bug11179Foo(string $p): string
{
\PHPStan\dumpType($p);
return "";
}
Loading