Skip to content

Commit da751f4

Browse files
committed
Fix missing detection of dead code in expressions
1 parent 5878035 commit da751f4

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
@@ -842,17 +842,20 @@ private function processStmtNode(
842842
} elseif ($stmt instanceof Echo_) {
843843
$hasYield = false;
844844
$throwPoints = [];
845+
$isAlwaysTerminating = false;
845846
foreach ($stmt->exprs as $echoExpr) {
846847
$result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep());
847848
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
848849
$scope = $result->getScope();
849850
$hasYield = $hasYield || $result->hasYield();
851+
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
850852
}
851853

852854
$throwPoints = $overridingThrowPoints ?? $throwPoints;
853855
$impurePoints = [
854856
new ImpurePoint($scope, $stmt, 'echo', 'echo', true),
855857
];
858+
return new StatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
856859
} elseif ($stmt instanceof Return_) {
857860
if ($stmt->expr !== null) {
858861
$result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
@@ -2410,6 +2413,7 @@ public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scop
24102413
return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context);
24112414
}
24122415

2416+
$isAlwaysTerminating = false;
24132417
$this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context);
24142418

24152419
if ($expr instanceof Variable) {
@@ -2593,6 +2597,7 @@ static function (): void {
25932597

25942598
if ($parametersAcceptor !== null) {
25952599
$expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
2600+
$isAlwaysTerminating = $parametersAcceptor->getReturnType() instanceof NeverType;
25962601
}
25972602
$result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
25982603
$scope = $result->getScope();
@@ -3302,6 +3307,7 @@ static function (): void {
33023307
$hasYield = $result->hasYield();
33033308
$throwPoints = $result->getThrowPoints();
33043309
$impurePoints = $result->getImpurePoints();
3310+
$isAlwaysTerminating = $result->isAlwaysTerminating();
33053311
$result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep());
33063312
if (
33073313
($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
@@ -3313,6 +3319,7 @@ static function (): void {
33133319
$hasYield = $hasYield || $result->hasYield();
33143320
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
33153321
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3322+
$isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
33163323
} elseif ($expr instanceof Expr\Include_) {
33173324
$result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
33183325
$throwPoints = $result->getThrowPoints();
@@ -3998,6 +4005,7 @@ static function (Node $node, Scope $scope) use ($nodeCallback): void {
39984005
$impurePoints,
39994006
static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
40004007
static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
4008+
$isAlwaysTerminating,
40014009
);
40024010
}
40034011

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)