Skip to content

Fix missing detection of dead code in expressions #4090

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

Open
wants to merge 25 commits into
base: 2.1.x
Choose a base branch
from
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
6 changes: 6 additions & 0 deletions src/Analyser/ExpressionResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ final class ExpressionResult
public function __construct(
private MutatingScope $scope,
private bool $hasYield,
private bool $isAlwaysTerminating,
private array $throwPoints,
private array $impurePoints,
?callable $truthyScopeCallback = null,
Expand Down Expand Up @@ -90,4 +91,9 @@ public function getFalseyScope(): MutatingScope
return $this->falseyScope;
}

public function isAlwaysTerminating(): bool
{
return $this->isAlwaysTerminating;
}

}
133 changes: 121 additions & 12 deletions src/Analyser/NodeScopeResolver.php

Large diffs are not rendered by default.

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

namespace PHPStan\Analyser;

use PhpParser\Node\Stmt;
use PHPStan\Parser\Parser;
use PHPStan\ShouldNotHappenException;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\StringType;
use PHPUnit\Framework\Attributes\DataProvider;
use function count;
use function get_class;
use function sprintf;

class ExpressionResultTest extends PHPStanTestCase
{

public static function dataIsAlwaysTerminating(): array
{
return [
[
'sprintf("hello %s", "abc");',
false,
],
[
'isset($x);',
false,
],
[
'$x ? "def" : "abc";',
false,
],
[
'(string) $x;',
false,
],
[
'$x || exit();',
false,
],
[
'$x ?? exit();',
false,
],
[
'sprintf("hello %s", exit());',
true,
],
[
'(string) exit();',
true,
],
[
'!exit();',
true,
],
[
'eval(exit());',
true,
],
[
'empty(exit());',
true,
],
[
'isset(exit());',
true,
],
[
'$x ? "abc" : exit();',
true,
],
[
'$x ? exit() : "abc";',
true,
],
[
'fn() => yield (exit());',
true,
],
[
'@exit();',
true,
],
[
'$x && exit();',
true,
],
[
'exit() && $x;',
true,
],
[
'exit() || $x;',
true,
],
[
'exit() ?? $x;',
true,
],
];
}

#[DataProvider('dataIsAlwaysTerminating')]
public function testIsAlwaysTerminating(
string $code,
bool $expectedIsAlwaysTerminating,
): void
{
/** @var Parser $parser */
$parser = self::getContainer()->getService('currentPhpVersionRichParser');

/** @var Stmt[] $stmts */
$stmts = $parser->parseString(sprintf('<?php %s', $code));
if (count($stmts) !== 1) {
throw new ShouldNotHappenException('Expecting code which evaluates to a single statement, got: ' . count($stmts));
}
if (!$stmts[0] instanceof Stmt\Expression) {
throw new ShouldNotHappenException('Expecting code contains a single statement expression, got: ' . get_class($stmts[0]));
}
$stmt = $stmts[0];
$expr = $stmt->expr;

/** @var NodeScopeResolver $nodeScopeResolver */
$nodeScopeResolver = self::getContainer()->getByType(NodeScopeResolver::class);
/** @var ScopeFactory $scopeFactory */
$scopeFactory = self::getContainer()->getByType(ScopeFactory::class);
$scope = $scopeFactory->create(ScopeContext::create('test.php'))
->assignVariable('string', new StringType(), new StringType(), TrinaryLogic::createYes())
->assignVariable('x', new IntegerType(), new IntegerType(), TrinaryLogic::createYes())
->assignVariable('cond', new MixedType(), new MixedType(), TrinaryLogic::createYes())
->assignVariable('arr', new ArrayType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), TrinaryLogic::createYes());

$result = $nodeScopeResolver->processExprNode(
$stmt,
$expr,
$scope,
static function (): void {
},
ExpressionContext::createTopLevel(),
);
$this->assertSame($expectedIsAlwaysTerminating, $result->isAlwaysTerminating());
}

}
97 changes: 97 additions & 0 deletions tests/PHPStan/Rules/DeadCode/UnreachableStatementRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;

/**
* @extends RuleTestCase<UnreachableStatementRule>
Expand Down Expand Up @@ -240,4 +241,100 @@ public function testMultipleUnreachable(): void
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug11909(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-11909.php'], [
[
'Unreachable statement - code above always terminates.',
10,
],
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug13232a(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-13232a.php'], [
[
'Unreachable statement - code above always terminates.',
10,
],
[
'Unreachable statement - code above always terminates.',
17,
],
[
'Unreachable statement - code above always terminates.',
23,
],
[
'Unreachable statement - code above always terminates.',
32,
],
[
'Unreachable statement - code above always terminates.',
38,
],
[
'Unreachable statement - code above always terminates.',
44,
],
[
'Unreachable statement - code above always terminates.',
52,
],
[
'Unreachable statement - code above always terminates.',
61,
],
[
'Unreachable statement - code above always terminates.',
70,
],
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug13232b(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-13232b.php'], [
[
'Unreachable statement - code above always terminates.',
19,
],
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug13232c(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-13232c.php'], [
[
'Unreachable statement - code above always terminates.',
12,
],
[
'Unreachable statement - code above always terminates.',
20,
],
]);
}

#[RequiresPhp('>= 8.1')]
public function testBug13232d(): void
{
$this->treatPhpDocTypesAsCertain = false;
$this->analyse([__DIR__ . '/data/bug-13232d.php'], [
[
'Unreachable statement - code above always terminates.',
11,
],
]);
}

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

namespace Bug11909;

function doFoo(): never {
throw new LogicException("throws");
}

echo doFoo();
echo "hello";
84 changes: 84 additions & 0 deletions tests/PHPStan/Rules/DeadCode/data/bug-13232a.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Bug13232a;

final class HelloWorld
{
public function sayHa(): void
{
echo sprintf("Hello, %s no way", $this->neverReturnsMethod());
echo 'this will never happen';
}

public function sayHi(): void
{
echo 'Hello, ' . neverReturns()
. ' no way';
echo 'this will never happen';
}

public function sayHo(): void
{
echo "Hello, {$this->neverReturnsMethod()} no way";
echo 'this will never happen';
}

public function sayHe(): void
{
$callable = function (): never {
exit();
};
echo sprintf("Hello, %s no way", $callable());
echo 'this will never happen';
}

public function sayHe2(): void
{
$this->doFoo($this->neverReturnsMethod());
echo 'this will never happen';
}

public function sayHe3(): void
{
self::doStaticFoo($this->neverReturnsMethod());
echo 'this will never happen';
}

public function sayHuu(): void
{
$x = [
$this->neverReturnsMethod()
];
echo 'this will never happen';
}

public function sayClosure(): void
{
$callable = function (): never {
exit();
};
$callable();
echo 'this will never happen';
}

public function sayIIFE(): void
{
(function (): never {
exit();
})();

echo 'this will never happen';
}

function neverReturnsMethod(): never {
exit();
}

public function doFoo() {}

static public function doStaticFoo() {}
}
function neverReturns(): never {
exit();
}

Loading
Loading