Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
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;
}

}
131 changes: 119 additions & 12 deletions src/Analyser/NodeScopeResolver.php

Large diffs are not rendered by default.

171 changes: 171 additions & 0 deletions tests/PHPStan/Analyser/ExpressionResultTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?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\IntegerType;
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();',
false,
],
[
'$x ? exit() : "abc";',
false,
],
[
'fn() => yield (exit());',
false,
],
[
'@exit();',
true,
],
[
'$x && exit();',
false,
],
[
'exit() && $x;',
true,
],
[
'exit() || $x;',
true,
],
[
'exit() ?? $x;',
true,
],
[
'var_dump(1+exit());',
true,
],
[
'var_dump(1-exit());',
true,
],
[
'var_dump(1*exit());',
true,
],
[
'var_dump(1**exit());',
true,
],
[
'var_dump(1/exit());',
true,
],
[
'var_dump("a".exit());',
true,
],
[
'var_dump(exit()."a");',
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('x', new IntegerType(), new IntegerType(), 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 @@ -251,4 +252,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