1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 18:36:58 +01:00
psalm/tests/UnusedCodeTest.php
2022-01-19 19:29:16 +01:00

1718 lines
54 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\CodeException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Provider\FakeFileProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Internal\RuntimeCaches;
use Psalm\IssueBuffer;
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
use function getcwd;
use function preg_quote;
use function strpos;
use const DIRECTORY_SEPARATOR;
class UnusedCodeTest extends TestCase
{
/** @var ProjectAnalyzer */
protected $project_analyzer;
public function setUp(): void
{
RuntimeCaches::clearAll();
$this->file_provider = new FakeFileProvider();
$this->project_analyzer = new ProjectAnalyzer(
new TestConfig(),
new Providers(
$this->file_provider,
new FakeParserCacheProvider()
)
);
$this->project_analyzer->getCodebase()->reportUnusedCode();
$this->project_analyzer->setPhpVersion('7.3', 'tests');
}
/**
* @dataProvider providerValidCodeParse
*
* @param string $code
* @param array<string> $error_levels
*
*/
public function testValidCode($code, array $error_levels = []): void
{
$test_name = $this->getTestName();
if (strpos($test_name, 'SKIPPED-') !== false) {
$this->markTestSkipped('Skipped due to a bug.');
}
$file_path = self::$src_dir_path . 'somefile.php';
$this->addFile(
$file_path,
$code
);
$this->project_analyzer->setPhpVersion('8.0', 'tests');
foreach ($error_levels as $error_level) {
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
}
$this->analyzeFile($file_path, new Context(), false);
$this->project_analyzer->consolidateAnalyzedData();
IssueBuffer::processUnusedSuppressions($this->project_analyzer->getCodebase()->file_provider);
}
/**
* @dataProvider providerInvalidCodeParse
*
* @param string $code
* @param string $error_message
* @param array<string> $error_levels
*
*/
public function testInvalidCode($code, $error_message, $error_levels = []): void
{
if (strpos($this->getTestName(), 'SKIPPED-') !== false) {
$this->markTestSkipped();
}
$this->expectException(CodeException::class);
$this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$file_path = self::$src_dir_path . 'somefile.php';
foreach ($error_levels as $error_level) {
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
}
$this->addFile(
$file_path,
$code
);
$this->analyzeFile($file_path, new Context(), false);
$this->project_analyzer->consolidateAnalyzedData();
IssueBuffer::processUnusedSuppressions($this->project_analyzer->getCodebase()->file_provider);
}
public function testSeesClassesUsedAfterUnevaluatedCodeIssue(): void
{
$this->project_analyzer->getConfig()->throw_exception = false;
$file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php';
$this->addFile(
$file_path,
'<?php
if (rand(0, 1)) {
throw new Exception("foo");
echo "bar";
} else {
$f = new Foo();
$f->bar();
}
class Foo {
function bar(): void{
echo "foo";
}
}
'
);
$this->analyzeFile($file_path, new Context(), false);
$this->project_analyzer->consolidateAnalyzedData();
$this->assertSame(1, IssueBuffer::getErrorCount());
$issue = IssueBuffer::getIssuesDataForFile($file_path)[0];
$this->assertSame('UnevaluatedCode', $issue->type);
$this->assertSame(4, $issue->line_from);
}
public function testSeesUnusedClassReferencedByUnevaluatedCode(): void
{
$this->project_analyzer->getConfig()->throw_exception = false;
$file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'somefile.php';
$this->addFile(
$file_path,
'<?php
if (rand(0, 1)) {
throw new Exception("foo");
$f = new Foo();
$f->bar();
} else {
echo "bar";
}
class Foo {
function bar(): void{
echo "foo";
}
}
'
);
$this->analyzeFile($file_path, new Context(), false);
$this->project_analyzer->consolidateAnalyzedData();
$this->assertSame(3, IssueBuffer::getErrorCount());
$issue = IssueBuffer::getIssuesDataForFile($file_path)[2];
$this->assertSame('UnusedClass', $issue->type);
$this->assertSame(10, $issue->line_from);
}
/**
* @return array<string, array{string}>
*/
public function providerValidCodeParse(): array
{
return [
'magicCall' => [
'<?php
class A {
/** @var string */
private $value = "default";
/** @param string[] $args */
public function __call(string $name, array $args) {
if (count($args) == 1) {
$this->modify($name, $args[0]);
}
}
private function modify(string $name, string $value): void {
call_user_func([$this, "modify" . $name], $value);
}
public function modifyFoo(string $value): void {
$this->value = $value;
}
public function getFoo() : string {
return $this->value;
}
}
$m = new A();
$m->foo("value");
$m->modifyFoo("value2");
echo $m->getFoo();',
],
'usedTraitMethodWithExplicitCall' => [
'<?php
class A {
public function foo(): void {
echo "parent method";
}
}
trait T {
public function foo(): void {
echo "trait method";
}
}
class B extends A {
use T;
}
(new A)->foo();
(new B)->foo();',
],
'usedInterfaceMethod' => [
'<?php
interface I {
public function foo(): void;
}
class A implements I {
public function foo(): void {}
}
(new A)->foo();',
],
'constructorIsUsed' => [
'<?php
class A {
public function __construct() {
$this->foo();
}
private function foo() : void {}
}
$a = new A();
echo (bool) $a;',
],
'everythingUsed' => [
'<?php
interface I {
public function foo() : void;
}
class B implements I {
public function foo() : void {}
}
class A
{
/**
* @var I
*/
private $i;
/**
* @param int[] $as
*/
public function __construct(array $as) {
$this->i = new B();
foreach ($as as $a) {
$this->a($a, 1);
}
}
private function a(int $a, int $b): void
{
$this->v($a, $b);
$this->i->foo();
}
private function v(int $a, int $b): void
{
if ($a + $b > 0) {
throw new \RuntimeException("");
}
}
}
new A([1, 2, 3]);',
],
'unusedParamWithUnderscore' => [
'<?php
function foo(int $_) : void {}
foo(4);',
],
'unusedParamWithUnusedPrefix' => [
'<?php
function foo(int $unusedArg) : void {}
foo(4);',
],
'usedFunctionCall' => [
'<?php
$a = strlen("goodbye");
echo $a;',
],
'possiblyUnusedParamWithUnderscore' => [
'<?php
class A {
public static function foo(int $_ = null) : void {}
}
A::foo();',
],
'possiblyUnusedParamWithUnusedPrefix' => [
'<?php
class A {
public static function foo(int $unusedArg = null) : void {}
}
A::foo();',
],
'usedClass' => [
'<?php
class A { }
new A();',
],
'usedTraitMethodWithImplicitCall' => [
'<?php
class A {
public function foo() : void {}
}
trait T {
public function foo() : void {}
}
class B extends A {
use T;
}
function takesA(A $a) : void {
$a->foo();
}
takesA(new B);',
],
'usedMethodInTryCatch' => [
'<?php
class A {
protected function getC() : C {
return new C;
}
}
class C {
public function foo() : void {}
}
class B extends A {
public function bar() : void {
$c = $this->getC();
foreach ([1, 2, 3] as $_) {
try {
$c->foo();
} catch (Exception $e) {}
}
}
}
(new B)->bar();',
],
'suppressPrivateUnusedMethod' => [
'<?php
class A {
/**
* @psalm-suppress UnusedMethod
* @return void
*/
private function foo() {}
}
new A();',
],
'abstractMethodImplementerCoveredByParentCall' => [
'<?php
abstract class Foobar {
public function doIt(): void {
$this->inner();
}
abstract protected function inner(): void;
}
class MyFooBar extends Foobar {
protected function inner(): void {
// Do nothing
}
}
$myFooBar = new MyFooBar();
$myFooBar->doIt();',
],
'methodUsedAsCallable' => [
'<?php
class C {
public static function foo() : void {}
}
function takesCallable(callable $c) : void {
$c();
}
takesCallable([C::class, "foo"]);',
],
'propertyAndMethodOverriddenDownstream' => [
'<?php
class A {
/** @var string */
public $foo = "hello";
public function bar() : void {}
}
class B extends A {
/** @var string */
public $foo = "goodbye";
public function bar() : void {}
}
function foo(A $a) : void {
echo $a->foo;
$a->bar();
}
foo(new B());',
],
'protectedPropertyOverriddenDownstream' => [
'<?php
class C {
protected int $foo = 1;
public function bar() : void {
$this->foo = 5;
}
public function getFoo(): void {
echo $this->foo;
}
}
class D extends C {
protected int $foo = 2;
}
(new D)->bar();
(new D)->getFoo();',
],
'usedClassAfterExtensionLoaded' => [
'<?php
class A {
public function __construct() {}
}
if (extension_loaded("fdsfdsfd")) {
new A();
}',
],
'usedParamInIf' => [
'<?php
class O {}
class C {
private bool $a = false;
public array $_types = [];
private static function mirror(array $a) : array {
return $a;
}
/**
* @param class-string<O>|null $type
*/
public function addType(?string $type, array $ids = array()): void
{
if ($this->a) {
$ids = self::mirror($ids);
}
$this->_types[$type ?: ""] = new ArrayObject($ids);
return;
}
}
(new C)->addType(null);',
],
'usedMethodAfterClassExists' => [
'<?php
class A {
public static function bar() : void {}
}
if (class_exists(A::class)) {
A::bar();
}',
],
'usedParamInLoopBeforeBreak' => [
'<?php
class Foo {}
function takesFoo(Foo $foo1, Foo $foo2): Foo {
while (rand(0, 1)) {
echo get_class($foo1);
if (rand(0, 1)) {
$foo1 = $foo2;
break;
}
}
return $foo1;
}',
],
'usedParamInLoopBeforeContinue' => [
'<?php
class Foo {}
function takesFoo(Foo $foo1, Foo $foo2): Foo {
while (rand(0, 1)) {
echo get_class($foo1);
if (rand(0, 1)) {
$foo1 = $foo2;
continue;
}
}
return $foo1;
}',
],
'usedParamInLoopBeforeWithChangeContinue' => [
'<?php
class Foo {}
class Bar {
public static function build(Foo $foo) : ?self {
echo get_class($foo);
return new self();
}
public function produceFoo(): Foo {
return new Foo();
}
}
function takesFoo(Foo $foo): Foo {
while (rand(0, 1)) {
$bar = Bar::build($foo);
if ($bar) {
$foo = $bar->produceFoo();
continue;
}
}
return $foo;
}',
],
'suppressUnusedMethod' => [
'<?php
class A {
/**
* @psalm-suppress UnusedMethod
*/
public function foo() : void {}
}
new A();'
],
'usedFunctionInCall' => [
'<?php
function fooBar(): void {}
$foo = "foo";
$bar = "bar";
($foo . ucfirst($bar))();',
],
'usedParamInUnknownMethodConcat' => [
'<?php
/**
* @psalm-suppress MixedMethodCall
*/
function foo(string $s, object $o) : void {
$o->foo("COUNT{$s}");
}'
],
'usedFunctioninMethodCallName' => [
'<?php
class Foo {
/**
* @psalm-suppress MixedArgument
*/
public function bar(string $request): void {
/** @var mixed $action */
$action = "";
$this->{"execute" . ucfirst($action)}($request);
}
}
(new Foo)->bar("request");'
],
'usedMethodCallForExternalMutationFreeClass' => [
'<?php
/**
* @psalm-external-mutation-free
*/
class A {
private string $foo;
public function __construct(string $foo) {
$this->foo = $foo;
}
public function setFoo(string $foo) : void {
$this->foo = $foo;
}
public function getFoo() : string {
return $this->foo;
}
}
$a = new A("hello");
$a->setFoo($a->getFoo() . "cool");',
],
'functionUsedAsArrayKeyInc' => [
'<?php
/** @param array<int, int> $arr */
function inc(array $arr) : array {
$arr[strlen("hello")]++;
return $arr;
}'
],
'pureFunctionUsesMethodBeforeReturning' => [
'<?php
/** @psalm-external-mutation-free */
class Counter {
private int $count = 0;
public function __construct(int $count) {
$this->count = $count;
}
public function increment() : void {
$this->count++;
}
}
/** @psalm-pure */
function makesACounter(int $i) : Counter {
$c = new Counter($i);
$c->increment();
return $c;
}',
],
'usedUsort' => [
'<?php
/** @param string[] $arr */
function foo(array $arr) : array {
usort($arr, "strnatcasecmp");
return $arr;
}'
],
'allowArrayMapWithClosure' => [
'<?php
$a = [1, 2, 3];
array_map(function($i) { echo $i;}, $a);'
],
'usedAssertFunction' => [
'<?php
/**
* @param mixed $v
* @psalm-pure
* @psalm-assert int $v
*/
function assertInt($v):void {
if (!is_int($v)) {
throw new \RuntimeException();
}
}
/**
* @psalm-pure
* @param mixed $i
*/
function takesMixed($i) : int {
assertInt($i);
return $i;
}'
],
'usedFunctionCallInsideSwitchWithTernary' => [
'<?php
function getArg(string $method) : void {
switch (strtolower($method ?: "")) {
case "post":
break;
case "get":
break;
default:
break;
}
}'
],
'ignoreSerializerSerialize' => [
'<?php
class Foo implements Serializable {
public function serialize() : string {
return "";
}
public function unserialize($_serialized) : void {}
}
new Foo();'
],
'useIteratorMethodsWhenCallingForeach' => [
'<?php
/** @psalm-suppress UnimplementedInterfaceMethod */
class IterableResult implements \Iterator {
public function current() {
return null;
}
public function key() {
return 5;
}
}
$items = new IterableResult();
foreach ($items as $_item) {}'
],
'usedThroughNewClassStringOfBase' => [
'<?php
/**
* @psalm-consistent-constructor
*/
abstract class FooBase {
public final function __construct() {}
public function baz() : void {
echo "hello";
}
}
/**
* @psalm-template T as FooBase
* @psalm-param class-string<T> $type
* @psalm-return T
*/
function createFoo($type): FooBase {
return new $type();
}
class Foo extends FooBase {}
createFoo(Foo::class)->baz();'
],
'usedMethodReferencedByString' => [
'<?php
class A {
static function b(): void {}
}
$methodRef = "A::b";
$methodRef();',
],
'usedMethodReferencedByStringWithLeadingBackslash' => [
'<?php
class A {
static function b(): void {}
}
$methodRef = "\A::b";
$methodRef();',
],
'arrayPushFunctionCall' => [
'<?php
$a = [];
array_push($a, strlen("hello"));
echo $a[0];'
],
'callMethodThatUpdatesStaticVar' => [
'<?php
class References {
/**
* @var array<string, string>
*/
public static $foo = [];
/**
* @param array<string, string> $map
*/
public function bar(array $map) : void {
self::$foo += $map;
}
}
(new References)->bar(["a" => "b"]);'
],
'promotedPropertyIsUsed' => [
'<?php
class Test {
public function __construct(public int $id, public string $name) {}
}
$test = new Test(1, "ame");
echo $test->id;
echo $test->name;'
],
'unusedNoReturnFunctionCall' => [
'<?php
/**
* @return no-return
*
* @pure
*
* @throws RuntimeException
*/
function invariant_violation(string $message): void
{
throw new RuntimeException($message);
}
/**
* @pure
*/
function reverse(string $string): string
{
if ("" === $string) {
invariant_violation("i do not like empty strings.");
}
return strrev($string);
}'
],
'unusedByReferenceFunctionCall' => [
'<?php
function bar(string &$str): string
{
$str .= "foo";
return $str;
}
function baz(): string
{
$f = "foo";
bar($f);
return $f;
}'
],
'unusedVoidByReferenceFunctionCall' => [
'<?php
function bar(string &$str): void
{
$str .= "foo";
}
function baz(): string
{
$f = "foo";
bar($f);
return $f;
}'
],
'unusedNamedByReferenceFunctionCall' => [
'<?php
function bar(string $c = "", string &$str = ""): string
{
$c .= $str;
$str .= $c;
return $c;
}
function baz(): string
{
$f = "foo";
bar(str: $f);
return $f;
}'
],
'unusedNamedByReferenceFunctionCallV2' => [
'<?php
function bar(string &$st, string &$str = ""): string
{
$st .= $str;
return $st;
}
function baz(): string
{
$f = "foo";
bar(st: $f);
return $f;
}',
],
'unusedNamedByReferenceFunctionCallV3' => [
'<?php
function bar(string &$st, ?string &$str = ""): string
{
$st .= (string) $str;
return $st;
}
function baz(): string
{
$f = "foo";
bar(st: $f, str: $c);
return $f;
}',
],
'functionCallUsedInThrow' => [
'<?php
/**
* @psalm-pure
*/
function getException(): \Exception
{
return new \Exception();
}
throw getException();'
],
'nullableMethodCallIsUsed' => [
'<?php
final class Test {
public function test(): void {
}
}
final class TestFactory {
/**
* @psalm-pure
*/
public function create(bool $returnNull): ?Test {
if ($returnNull) {
return null;
}
return new Test();
}
}
$factory = new TestFactory();
$factory->create(false)?->test();
$exception = new \Exception();
throw ($exception->getPrevious() ?? $exception);'
],
'publicPropertyReadInFile' => [
'<?php
class A {
public string $a;
public function __construct() {
$this->a = "hello";
}
}
$foo = new A();
echo $foo->a;',
],
'publicPropertyReadInMethod' => [
'<?php
class A {
public string $a = "hello";
}
class B {
public function foo(A $a): void {
if ($a->a === "goodbye") {}
}
}
(new B)->foo(new A());',
],
'privatePropertyReadInMethod' => [
'<?php
class A {
private string $a;
public function __construct() {
$this->a = "hello";
}
public function emitA(): void {
echo $this->a;
}
}
(new A())->emitA();',
],
'fluentMethodsAllowed' => [
'<?php
class A {
public function foo(): static {
return $this;
}
public function bar(): static {
return $this;
}
}
(new A())->foo()->bar();',
],
'unusedInterfaceReturnValueWithImplementingClassSuppressed' => [
'<?php
interface IWorker {
/** @psalm-suppress PossiblyUnusedReturnValue */
public function work(): bool;
}
class Worker implements IWorker{
public function work(): bool {
return true;
}
}
function f(IWorker $worker): void {
$worker->work();
}
f(new Worker());',
],
'interfaceReturnValueWithImplementingAndAbstractClass' => [
'<?php
interface IWorker {
public function work(): int;
}
class AbstractWorker implements IWorker {
public function work(): int {
return 0;
}
}
class Worker extends AbstractWorker {
public function work(): int {
return 1;
}
}
class AnotherWorker extends AbstractWorker {}
function f(IWorker $worker): void {
echo $worker->work();
}
f(new Worker());
f(new AnotherWorker());',
],
'methodReturnValueUsedInThrow' => [
'<?php
class A {
public function foo() : Exception {
return new Exception;
}
}
throw (new A)->foo();
'
],
'staticMethodReturnValueUsedInThrow' => [
'<?php
class A {
public static function foo() : Exception {
return new Exception;
}
}
throw A::foo();
'
],
'variableUsedAsUnaryMinusOperand' => [
'<?php
function f(): int
{
$a = 1;
$b = -$a;
return $b;
}
',
],
'variableUsedAsUnaryPlusOperand' => [
'<?php
function f(): int
{
$a = 1;
$b = +$a;
return $b;
}
',
],
'variableUsedInBacktick' => [
'<?php
$used = "echo";
/** @psalm-suppress ForbiddenCode */
`$used`;
',
],
'notUnevaluatedFunction' => [
'<?php
/** @return never */
function neverReturns(){
die();
}
unrelated();
neverReturns();
function unrelated():void{
echo "hello";
}',
],
'NotUnusedWhenAssert' => [
'<?php
class A {
public function getVal(?string $val): string {
$this->assert($val);
return $val;
}
/**
* @psalm-assert string $val
* @psalm-mutation-free
*/
private function assert(?string $val): void {
if (null === $val) {
throw new Exception();
}
}
}
$a = new A();
echo $a->getVal(null);',
],
'NotUnusedWhenThrows' => [
'<?php
declare(strict_types=1);
/** @psalm-immutable */
final class UserList
{
/**
* @throws InvalidArgumentException
*/
public function validate(): void
{
// Some validation happens here
throw new \InvalidArgumentException();
}
}
$a = new UserList();
$a->validate();
',
],
'__halt_compiler_no_usage_check' => [
'<?php
exit(0);
__halt_compiler();
foobar
',
],
'usedPropertyAsAssignmentKey' => [
'<?php
class A {
public string $foo = "bar";
public array $bar = [];
}
$a = new A();
$a->bar[$a->foo] = "bar";
print_r($a->bar);',
],
];
}
/**
* @return array<string,array{string,error_message:string}>
*/
public function providerInvalidCodeParse(): array
{
return [
'unusedClass' => [
'<?php
class A { }',
'error_message' => 'UnusedClass',
],
'publicUnusedMethod' => [
'<?php
class A {
/** @return void */
public function foo() {}
}
new A();',
'error_message' => 'PossiblyUnusedMethod',
],
'possiblyUnusedParam' => [
'<?php
class A {
/** @return void */
public function foo(int $i) {}
}
(new A)->foo(4);',
'error_message' => 'PossiblyUnusedParam - src' . DIRECTORY_SEPARATOR
. 'somefile.php:4:49 - Param #1 is never referenced in this method',
],
'unusedParam' => [
'<?php
function foo(int $i) {}
foo(4);',
'error_message' => 'UnusedParam',
],
'possiblyUnusedProperty' => [
'<?php
class A {
/** @var string */
public $foo = "hello";
}
$a = new A();',
'error_message' => 'PossiblyUnusedProperty',
'error_levels' => ['UnusedVariable'],
],
'possiblyUnusedPropertyWrittenNeverRead' => [
'<?php
class A {
/** @var string */
public $foo = "hello";
}
$a = new A();
$a->foo = "bar";',
'error_message' => 'PossiblyUnusedProperty',
'error_levels' => ['UnusedVariable'],
],
'possiblyUnusedPropertyWithArrayWrittenNeverRead' => [
'<?php
class A {
/** @var list<string> */
public array $foo = [];
}
$a = new A();
$a->foo[] = "bar";',
'error_message' => 'PossiblyUnusedProperty',
'error_levels' => ['UnusedVariable'],
],
'unusedProperty' => [
'<?php
class A {
/** @var string */
private $foo = "hello";
}
$a = new A();',
'error_message' => 'UnusedProperty',
'error_levels' => ['UnusedVariable'],
],
'privateUnusedMethod' => [
'<?php
class A {
/** @return void */
private function foo() {}
}
new A();',
'error_message' => 'UnusedMethod',
],
'unevaluatedCode' => [
'<?php
function foo(): void {
return;
$a = "foo";
}',
'error_message' => 'UnevaluatedCode',
],
'unusedTraitMethodInParent' => [
'<?php
class A {
public function foo() : void {}
}
trait T {
public function foo() : void {}
public function bar() : void {}
}
class B extends A {
use T;
}
function takesA(A $a) : void {
$a->foo();
}
takesA(new B);',
'error_message' => 'PossiblyUnusedMethod',
],
'unusedRecursivelyUsedMethod' => [
'<?php
class C {
public function foo() : void {
if (rand(0, 1)) {
$this->foo();
}
}
public function bar() : void {}
}
(new C)->bar();',
'error_message' => 'PossiblyUnusedMethod',
],
'unusedRecursivelyUsedStaticMethod' => [
'<?php
class C {
public static function foo() : void {
if (rand(0, 1)) {
self::foo();
}
}
public function bar() : void {}
}
(new C)->bar();',
'error_message' => 'PossiblyUnusedMethod',
],
'unusedFunctionCall' => [
'<?php
strlen("goodbye");',
'error_message' => 'UnusedFunctionCall',
],
'unusedMethodCallSimple' => [
'<?php
final class A {
private string $foo;
public function __construct(string $foo) {
$this->foo = $foo;
}
public function getFoo() : string {
return $this->foo;
}
}
$a = new A("hello");
$a->getFoo();',
'error_message' => 'UnusedMethodCall',
],
'propertyOverriddenDownstreamAndNotUsed' => [
'<?php
class A {
/** @var string */
public $foo = "hello";
}
class B extends A {
/** @var string */
public $foo = "goodbye";
}
new B();',
'error_message' => 'PossiblyUnusedProperty',
],
'propertyUsedOnlyInConstructor' => [
'<?php
class A {
/** @var int */
private $used;
/** @var int */
private $unused;
/** @var int */
private static $staticUnused;
public function __construct() {
$this->used = 4;
$this->unused = 4;
self::$staticUnused = 4;
}
public function handle(): void
{
$this->used++;
}
}
(new A())->handle();',
'error_message' => 'UnusedProperty',
],
'unusedMethodCallForExternalMutationFreeClass' => [
'<?php
/**
* @psalm-external-mutation-free
*/
class A {
private string $foo;
public function __construct(string $foo) {
$this->foo = $foo;
}
public function setFoo(string $foo) : void {
$this->foo = $foo;
}
}
function foo() : void {
(new A("hello"))->setFoo("goodbye");
}',
'error_message' => 'UnusedMethodCall',
],
'unusedMethodCallForGeneratingMethod' => [
'<?php
/**
* @psalm-external-mutation-free
*/
class A {
private string $foo;
public function __construct(string $foo) {
$this->foo = $foo;
}
public function getFoo() : string {
return "abular" . $this->foo;
}
}
/**
* @psalm-pure
*/
function makeA(string $s) : A {
return new A($s);
}
function foo() : void {
makeA("hello")->getFoo();
}',
'error_message' => 'UnusedMethodCall',
],
'annotatedMutationFreeUnused' => [
'<?php
class A {
private string $s;
public function __construct(string $s) {
$this->s = $s;
}
/** @psalm-mutation-free */
public function getShort() : string {
return substr($this->s, 0, 5);
}
}
$a = new A("hello");
$a->getShort();',
'error_message' => 'UnusedMethodCall',
],
'dateTimeImmutable' => [
'<?php
function foo(DateTimeImmutable $dt) : void {
$dt->modify("+1 day");
}',
'error_message' => 'UnusedMethodCall',
],
'unusedClassReferencesItself' => [
'<?php
class A {}
class AChild extends A {
public function __construct() {
self::foo();
}
public static function foo() : void {}
}',
'error_message' => 'UnusedClass',
],
'returnInBothIfConditions' => [
'<?php
function doAThing(): bool {
if (rand(0, 1)) {
return true;
} else {
return false;
}
return false;
}',
'error_message' => 'UnevaluatedCode',
],
'unevaluatedCodeAfterReturnInFinally' => [
'<?php
function noOp(): void {
return;
}
function doAThing(): bool {
try {
noOp();
} finally {
return true;
}
return false;
}',
'error_message' => 'UnevaluatedCode',
],
'UnusedFunctionCallWithOptionalByReferenceParameter' => [
'<?php
/**
* @pure
*/
function bar(string $c, string &$str = ""): string
{
$c .= $str;
return $c;
}
/**
* @pure
*/
function baz(): string
{
$f = "foo";
bar($f);
return $f;
}',
'error_message' => 'UnusedFunctionCall',
],
'UnusedFunctionCallWithOptionalByReferenceParameterV2' => [
'<?php
/**
* @pure
*/
function bar(string $st, string &$str = ""): string
{
$st .= $str;
return $st;
}
/**
* @pure
*/
function baz(): string
{
$f = "foo";
bar(st: $f);
return $f;
}',
'error_message' => 'UnusedFunctionCall',
],
'propertyWrittenButNotRead' => [
'<?php
class A {
public string $a = "hello";
public string $b = "world";
public function __construct() {
$this->a = "hello";
$this->b = "world";
}
}
$foo = new A();
echo $foo->a;',
'error_message' => 'PossiblyUnusedProperty',
],
'unusedInterfaceReturnValue' => [
'<?php
interface I {
public function work(): bool;
}
function f(I $worker): void {
$worker->work();
}',
'error_message' => 'PossiblyUnusedReturnValue',
],
'unusedInterfaceReturnValueWithImplementingClass' => [
'<?php
interface IWorker {
public function work(): bool;
}
class Worker implements IWorker{
public function work(): bool {
return true;
}
}
function f(IWorker $worker): void {
$worker->work();
}
f(new Worker());',
'error_message' => 'PossiblyUnusedReturnValue',
],
'interfaceWithImplementingClassMethodUnused' => [
'<?php
interface IWorker {
public function work(): void;
}
class Worker implements IWorker {
public function work(): void {}
}
function f(IWorker $worker): void {
echo get_class($worker);
}
f(new Worker());',
'error_message' => 'PossiblyUnusedMethod',
],
'UnusedFunctionInDoubleConditional' => [
'<?php
$list = [];
if (rand(0,1) && rand(0,1)) {
array_merge($list, []);
};
',
'error_message' => 'UnusedFunctionCall',
],
'functionNeverUnevaluatedCode' => [
'<?php
/** @return never */
function neverReturns() {
die();
}
function f(): void {
neverReturns();
echo "hello";
}
',
'error_message' => 'UnevaluatedCode',
],
'methodNeverUnevaluatedCode' => [
'<?php
class A{
/** @return never */
function neverReturns() {
die();
}
function f(): void {
$this->neverReturns();
echo "hello";
}
}
',
'error_message' => 'UnevaluatedCode',
],
'exitNeverUnevaluatedCode' => [
'<?php
function f(): void {
exit();
echo "hello";
}
',
'error_message' => 'UnevaluatedCode',
],
'exitInlineHtml' => [
'<?php
exit(0);
?'.'>foo
',
'error_message' => 'UnevaluatedCode',
],
];
}
}