Skip to content

Commit ddb0a5c

Browse files
committed
Fix missing detection of dead code in expressions
1 parent 5d8c209 commit ddb0a5c

File tree

5 files changed

+85
-0
lines changed

5 files changed

+85
-0
lines changed

src/Analyser/ExpressionResult.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function __construct(
2828
private array $impurePoints,
2929
?callable $truthyScopeCallback = null,
3030
?callable $falseyScopeCallback = null,
31+
private bool $isAlwaysTerminating = false,
3132
)
3233
{
3334
$this->truthyScopeCallback = $truthyScopeCallback;
@@ -90,4 +91,9 @@ public function getFalseyScope(): MutatingScope
9091
return $this->falseyScope;
9192
}
9293

94+
public function isAlwaysTerminating(): bool
95+
{
96+
return $this->isAlwaysTerminating;
97+
}
98+
9399
}

src/Analyser/NodeScopeResolver.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,17 +840,20 @@ private function processStmtNode(
840840
} elseif ($stmt instanceof Echo_) {
841841
$hasYield = false;
842842
$throwPoints = [];
843+
$isAlwaysTerminating = false;
843844
foreach ($stmt->exprs as $echoExpr) {
844845
$result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep());
845846
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
846847
$scope = $result->getScope();
847848
$hasYield = $hasYield || $result->hasYield();
849+
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
848850
}
849851

850852
$throwPoints = $overridingThrowPoints ?? $throwPoints;
851853
$impurePoints = [
852854
new ImpurePoint($scope, $stmt, 'echo', 'echo', true),
853855
];
856+
return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
854857
} elseif ($stmt instanceof Return_) {
855858
if ($stmt->expr !== null) {
856859
$result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
@@ -2408,6 +2411,7 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop
24082411
return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context);
24092412
}
24102413

2414+
$isAlwaysTerminating = false;
24112415
$this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context);
24122416

24132417
if ($expr instanceof Variable) {
@@ -2591,6 +2595,7 @@ static function (): void {
25912595

25922596
if ($parametersAcceptor !== null) {
25932597
$expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
2598+
$isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType;
25942599
}
25952600
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
25962601
$scope = $result->getScope();
@@ -3299,6 +3304,7 @@ static function (): void {
32993304
$hasYield = $result->hasYield();
33003305
$throwPoints = $result->getThrowPoints();
33013306
$impurePoints = $result->getImpurePoints();
3307+
$isAlwaysTerminating = $result->isAlwaysTerminating();
33023308
$result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep());
33033309
if (
33043310
($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
@@ -3310,6 +3316,7 @@ static function (): void {
33103316
$hasYield = $hasYield || $result->hasYield();
33113317
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
33123318
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3319+
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
33133320
} elseif ($expr instanceof Expr\Include_) {
33143321
$result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
33153322
$throwPoints = $result->getThrowPoints();
@@ -3995,6 +4002,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
39954002
$impurePoints,
39964003
static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
39974004
static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
4005+
$isAlwaysTerminating,
39984006
);
39994007
}
40004008

tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,26 @@ public function testMultipleUnreachable(): void
240240
]);
241241
}
242242

243+
public function testBug13232a(): void
244+
{
245+
$this->treatPhpDocTypesAsCertain = false;
246+
$this->analyse([__DIR__ . '/data/bug-13232a.php'], [
247+
[
248+
'Unreachable statement - code above always terminates.',
249+
11,
250+
],
251+
]);
252+
}
253+
254+
public function testBug13232b(): void
255+
{
256+
$this->treatPhpDocTypesAsCertain = false;
257+
$this->analyse([__DIR__ . '/data/bug-13232b.php'], [
258+
[
259+
'Unreachable statement - code above always terminates.',
260+
19,
261+
],
262+
]);
263+
}
264+
243265
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Bug13232a;
4+
5+
final class HelloWorld
6+
{
7+
public function sayHi(): void
8+
{
9+
echo 'Hello, ' . neverReturns()
10+
. ' no way';
11+
echo 'this will never happen';
12+
}
13+
}
14+
function neverReturns(): never {}
15+
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Bug13232b;
4+
5+
final class HelloWorld
6+
{
7+
public function sayHello(): void
8+
{
9+
if (rand(0, 1)) {
10+
$x = new Foo();
11+
} else {
12+
$x = '1';
13+
}
14+
echo 'Hello, ' . mightReturnNever($x)
15+
. ' no way';
16+
17+
echo 'Hello, ' . mightReturnNever(new Foo())
18+
. ' no way';
19+
echo 'this will never happen';
20+
}
21+
}
22+
23+
/** @phpstan-return ($x is Foo ? never : string) */
24+
function mightReturnNever(mixed $x)
25+
{
26+
if ($x instanceof Foo) {
27+
throw new LogicException();
28+
}
29+
return "hello";
30+
}
31+
32+
class Foo
33+
{
34+
}

0 commit comments

Comments
 (0)