1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-11 08:49:52 +01:00
psalm/tests/UnusedCodeTest.php
2023-11-13 20:37:48 +01:00

1948 lines
63 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
{
protected ProjectAnalyzer $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 array<string> $ignored_issues
*/
public function testValidCode(string $code, array $ignored_issues = []): 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 ($ignored_issues 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 array<string> $ignored_issues
*/
public function testInvalidCode(string $code, string $error_message, array $ignored_issues = []): 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 ($ignored_issues 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{code:string}>
*/
public function providerValidCodeParse(): array
{
return [
'magicCall' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
interface I {
public function foo(): void;
}
class A implements I {
public function foo(): void {}
}
(new A)->foo();',
],
'constructorIsUsed' => [
'code' => '<?php
class A {
public function __construct() {
$this->foo();
}
private function foo() : void {}
}
$a = new A();
echo (bool) $a;',
],
'everythingUsed' => [
'code' => '<?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' => [
'code' => '<?php
function foo(int $_) : void {}
foo(4);',
],
'unusedParamWithUnusedPrefix' => [
'code' => '<?php
function foo(int $unusedArg) : void {}
foo(4);',
],
'usedFunctionCall' => [
'code' => '<?php
$a = strlen("goodbye");
echo $a;',
],
'possiblyUnusedParamWithUnderscore' => [
'code' => '<?php
class A {
public static function foo(int $_ = null) : void {}
}
A::foo();',
],
'possiblyUnusedParamWithUnusedPrefix' => [
'code' => '<?php
class A {
public static function foo(int $unusedArg = null) : void {}
}
A::foo();',
],
'usedClass' => [
'code' => '<?php
class A { }
new A();',
],
'usedTraitMethodWithImplicitCall' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
class A {
/**
* @psalm-suppress UnusedMethod
* @return void
*/
private function foo() {}
}
new A();',
],
'abstractMethodImplementerCoveredByParentCall' => [
'code' => '<?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' => [
'code' => '<?php
class C {
public static function foo() : void {}
}
function takesCallable(callable $c) : void {
$c();
}
takesCallable([C::class, "foo"]);',
],
'propertyAndMethodOverriddenDownstream' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
class A {
public function __construct() {}
}
if (extension_loaded("fdsfdsfd")) {
new A();
}',
],
'useMethodPropertiesAfterExtensionLoaded' => [
'code' => '<?php
final class a {
public static self $a;
public static function get(): a {
return new a;
}
}
final class b {
public function test(): a {
return new a;
}
}
function process(b $handler): a {
if (\extension_loaded("fdsfdsfd")) {
return $handler->test();
}
if (\extension_loaded("fdsfdsfd")) {
return a::$a;
}
if (\extension_loaded("fdsfdsfd")) {
return a::get();
}
return $handler->test();
}',
],
'usedParamInIf' => [
'code' => '<?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' => [
'code' => '<?php
class A {
public static function bar() : void {}
}
if (class_exists(A::class)) {
A::bar();
}',
],
'usedParamInLoopBeforeBreak' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
class A {
/**
* @psalm-suppress UnusedMethod
*/
public function foo() : void {}
}
new A();',
],
'usedFunctionInCall' => [
'code' => '<?php
function fooBar(): void {}
$foo = "foo";
$bar = "bar";
($foo . ucfirst($bar))();',
],
'usedParamInUnknownMethodConcat' => [
'code' => '<?php
/**
* @psalm-suppress MixedMethodCall
*/
function foo(string $s, object $o) : void {
$o->foo("COUNT{$s}");
}',
],
'usedFunctioninMethodCallName' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
/** @param array<int, int> $arr */
function inc(array $arr) : array {
$arr[strlen("hello")]++;
return $arr;
}',
],
'pureFunctionUsesMethodBeforeReturning' => [
'code' => '<?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;
}',
],
'setRawCookieImpure' => [
'code' => '<?php
setrawcookie(
"name",
"value",
);',
],
'usedUsort' => [
'code' => '<?php
/** @param string[] $arr */
function foo(array $arr) : array {
usort($arr, "strnatcasecmp");
return $arr;
}',
],
'allowArrayMapWithClosure' => [
'code' => '<?php
$a = [1, 2, 3];
array_map(function($i) { echo $i;}, $a);',
],
'usedAssertFunction' => [
'code' => '<?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;
}',
],
'usedFunctionCallInEval' => [
'code' => '<?php
eval(str_repeat("a", 10));',
],
'usedFunctionCallInsideSwitchWithTernary' => [
'code' => '<?php
function getArg(string $method) : void {
switch (strtolower($method ?: "")) {
case "post":
break;
case "get":
break;
default:
break;
}
}',
],
'ignoreSerializerSerialize' => [
'code' => '<?php
class Foo implements Serializable {
public function serialize() : string {
return "";
}
public function unserialize($_serialized) : void {}
}
new Foo();',
],
'ignoreSerializeAndUnserialize' => [
'code' => '<?php
class Foo
{
public function __sleep(): array
{
throw new BadMethodCallException();
}
public function __wakeup(): void
{
throw new BadMethodCallException();
}
}
function test(Foo|int $foo, mixed $bar, iterable $baz): bool {
try {
serialize(new Foo());
serialize([new Foo()]);
serialize([[new Foo()]]);
serialize($foo);
serialize($bar);
serialize($baz);
unserialize("");
} catch (\Throwable) {
return false;
}
return true;
}',
],
'useIteratorMethodsWhenCallingForeach' => [
'code' => '<?php
/** @psalm-suppress UnimplementedInterfaceMethod, MissingTemplateParam */
class IterableResult implements \Iterator {
public function current() {
return null;
}
public function key() {
return 5;
}
}
$items = new IterableResult();
foreach ($items as $_item) {}',
],
'usedThroughNewClassStringOfBase' => [
'code' => '<?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' => [
'code' => '<?php
class A {
static function b(): void {}
}
$methodRef = "A::b";
$methodRef();',
],
'usedMethodReferencedByStringWithLeadingBackslash' => [
'code' => '<?php
class A {
static function b(): void {}
}
$methodRef = "\A::b";
$methodRef();',
],
'arrayPushFunctionCall' => [
'code' => '<?php
$a = [];
array_push($a, strlen("hello"));
echo $a[0];',
],
'callMethodThatUpdatesStaticVar' => [
'code' => '<?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' => [
'code' => '<?php
class Test {
public function __construct(public int $id, public string $name) {}
}
$test = new Test(1, "ame");
echo $test->id;
echo $test->name;',
],
'unusedNoReturnFunctionCall' => [
'code' => '<?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' => [
'code' => '<?php
function bar(string &$str): string
{
$str .= "foo";
return $str;
}
function baz(): string
{
$f = "foo";
bar($f);
return $f;
}',
],
'unusedVoidByReferenceFunctionCall' => [
'code' => '<?php
function bar(string &$str): void
{
$str .= "foo";
}
function baz(): string
{
$f = "foo";
bar($f);
return $f;
}',
],
'unusedNamedByReferenceFunctionCall' => [
'code' => '<?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' => [
'code' => '<?php
function bar(string &$st, string &$str = ""): string
{
$st .= $str;
return $st;
}
function baz(): string
{
$f = "foo";
bar(st: $f);
return $f;
}',
],
'unusedNamedByReferenceFunctionCallV3' => [
'code' => '<?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' => [
'code' => '<?php
/**
* @psalm-pure
*/
function getException(): \Exception
{
return new \Exception();
}
throw getException();',
],
'nullableMethodCallIsUsed' => [
'code' => '<?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' => [
'code' => '<?php
class A {
public string $a;
public function __construct() {
$this->a = "hello";
}
}
$foo = new A();
echo $foo->a;',
],
'publicPropertyReadInMethod' => [
'code' => '<?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' => [
'code' => '<?php
class A {
private string $a;
public function __construct() {
$this->a = "hello";
}
public function emitA(): void {
echo $this->a;
}
}
(new A())->emitA();',
],
'fluentMethodsAllowed' => [
'code' => '<?php
class A {
public function foo(): static {
return $this;
}
public function bar(): static {
return $this;
}
}
(new A())->foo()->bar();',
],
'unusedInterfaceReturnValueWithImplementingClassSuppressed' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
class A {
public function foo() : Exception {
return new Exception;
}
}
throw (new A)->foo();
',
],
'staticMethodReturnValueUsedInThrow' => [
'code' => '<?php
class A {
public static function foo() : Exception {
return new Exception;
}
}
throw A::foo();
',
],
'variableUsedAsUnaryMinusOperand' => [
'code' => '<?php
function f(): int
{
$a = 1;
$b = -$a;
return $b;
}
',
],
'variableUsedAsUnaryPlusOperand' => [
'code' => '<?php
function f(): int
{
$a = 1;
$b = +$a;
return $b;
}
',
],
'variableUsedInBacktick' => [
'code' => '<?php
$used = "echo";
/** @psalm-suppress ForbiddenCode */
`$used`;
',
],
'notUnevaluatedFunction' => [
'code' => '<?php
/** @return never */
function neverReturns(){
die();
}
unrelated();
neverReturns();
function unrelated():void{
echo "hello";
}',
],
'NotUnusedWhenAssert' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
exit(0);
__halt_compiler();
foobar
',
],
'usedPropertyAsAssignmentKey' => [
'code' => '<?php
class A {
public string $foo = "bar";
public array $bar = [];
}
$a = new A();
$a->bar[$a->foo] = "bar";
print_r($a->bar);',
],
'psalm-api with unused class' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {}
PHP,
],
'psalm-api with unused public and protected property' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {
public int $b = 0;
protected int $c = 0;
}
PHP,
],
'psalm-api with unused public and protected method' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {
public function b(): void {}
protected function c(): void {}
}
PHP,
],
'psalm-api on unused public method' => [
'code' => <<<'PHP'
<?php
class A {
/** @psalm-api */
public function b(): void {}
}
new A;
PHP,
],
'api with unused class' => [
'code' => <<<'PHP'
<?php
/** @api */
class A {}
PHP,
],
'api on unused public method' => [
'code' => <<<'PHP'
<?php
class A {
/** @api */
public function b(): void {}
}
new A;
PHP,
],
'callNeverReturnsSuppressed' => [
'code' => '<?php
namespace Foo;
/**
* @psalm-suppress InvalidReturnType
* @return never
*/
function foo() : void {}
/** @psalm-suppress NoValue */
$a = foo();
print_r($a);',
],
'useNeverReturnsAsArgSuppressed' => [
'code' => '<?php
namespace Foo;
/**
* @psalm-suppress InvalidReturnType
* @return never
*/
function foo() : void {}
/** @psalm-suppress UnusedParam */
function bar(string $s) : void {}
/** @psalm-suppress NoValue */
bar(foo());
echo "hello";',
],
];
}
/**
* @return array<string,array{code:string,error_message:string,ignored_issues?:list<string>}>
*/
public function providerInvalidCodeParse(): array
{
return [
'unusedClass' => [
'code' => '<?php
class A { }',
'error_message' => 'UnusedClass',
],
'publicUnusedMethod' => [
'code' => '<?php
class A {
/** @return void */
public function foo() {}
}
new A();',
'error_message' => 'PossiblyUnusedMethod',
],
'possiblyUnusedParam' => [
'code' => '<?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' => [
'code' => '<?php
function foo(int $i) {}
foo(4);',
'error_message' => 'UnusedParam',
],
'possiblyUnusedProperty' => [
'code' => '<?php
class A {
/** @var string */
public $foo = "hello";
}
$a = new A();',
'error_message' => 'PossiblyUnusedProperty',
'ignored_issues' => ['UnusedVariable'],
],
'possiblyUnusedPropertyWrittenNeverRead' => [
'code' => '<?php
class A {
/** @var string */
public $foo = "hello";
}
$a = new A();
$a->foo = "bar";',
'error_message' => 'PossiblyUnusedProperty',
'ignored_issues' => ['UnusedVariable'],
],
'possiblyUnusedPropertyWithArrayWrittenNeverRead' => [
'code' => '<?php
class A {
/** @var list<string> */
public array $foo = [];
}
$a = new A();
$a->foo[] = "bar";',
'error_message' => 'PossiblyUnusedProperty',
'ignored_issues' => ['UnusedVariable'],
],
'unusedProperty' => [
'code' => '<?php
class A {
/** @var string */
private $foo = "hello";
}
$a = new A();',
'error_message' => 'UnusedProperty',
'ignored_issues' => ['UnusedVariable'],
],
'privateUnusedMethod' => [
'code' => '<?php
class A {
/** @return void */
private function foo() {}
}
new A();',
'error_message' => 'UnusedMethod',
],
'unevaluatedCode' => [
'code' => '<?php
function foo(): void {
return;
$a = "foo";
}',
'error_message' => 'UnevaluatedCode',
],
'unusedTraitMethodInParent' => [
'code' => '<?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' => [
'code' => '<?php
class C {
public function foo() : void {
if (rand(0, 1)) {
$this->foo();
}
}
public function bar() : void {}
}
(new C)->bar();',
'error_message' => 'PossiblyUnusedMethod',
],
'unusedRecursivelyUsedStaticMethod' => [
'code' => '<?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' => [
'code' => '<?php
strlen("goodbye");',
'error_message' => 'UnusedFunctionCall',
],
'unusedMethodCallSimple' => [
'code' => '<?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' => [
'code' => '<?php
class A {
/** @var string */
public $foo = "hello";
}
class B extends A {
/** @var string */
public $foo = "goodbye";
}
new B();',
'error_message' => 'PossiblyUnusedProperty',
],
'propertyUsedOnlyInConstructor' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
function foo(DateTimeImmutable $dt) : void {
$dt->modify("+1 day");
}',
'error_message' => 'UnusedMethodCall',
],
'unusedClassReferencesItself' => [
'code' => '<?php
class A {}
class AChild extends A {
public function __construct() {
self::foo();
}
public static function foo() : void {}
}',
'error_message' => 'UnusedClass',
],
'returnInBothIfConditions' => [
'code' => '<?php
function doAThing(): bool {
if (rand(0, 1)) {
return true;
} else {
return false;
}
return false;
}',
'error_message' => 'UnevaluatedCode',
],
'unevaluatedCodeAfterReturnInFinally' => [
'code' => '<?php
function noOp(): void {
return;
}
function doAThing(): bool {
try {
noOp();
} finally {
return true;
}
return false;
}',
'error_message' => 'UnevaluatedCode',
],
'UnusedFunctionCallWithOptionalByReferenceParameter' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
interface I {
public function work(): bool;
}
function f(I $worker): void {
$worker->work();
}',
'error_message' => 'PossiblyUnusedReturnValue',
],
'unusedInterfaceReturnValueWithImplementingClass' => [
'code' => '<?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' => [
'code' => '<?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' => [
'code' => '<?php
$list = [];
if (rand(0,1) && rand(0,1)) {
array_merge($list, []);
};
',
'error_message' => 'UnusedFunctionCall',
],
'functionNeverUnevaluatedCode' => [
'code' => '<?php
/** @return never */
function neverReturns() {
die();
}
function f(): void {
neverReturns();
echo "hello";
}
',
'error_message' => 'UnevaluatedCode',
],
'methodNeverUnevaluatedCode' => [
'code' => '<?php
class A{
/** @return never */
function neverReturns() {
die();
}
function f(): void {
$this->neverReturns();
echo "hello";
}
}
',
'error_message' => 'UnevaluatedCode',
],
'exitNeverUnevaluatedCode' => [
'code' => '<?php
function f(): void {
exit();
echo "hello";
}
',
'error_message' => 'UnevaluatedCode',
],
'exitInlineHtml' => [
'code' => '<?php
exit(0);
?' . '>foo
',
'error_message' => 'UnevaluatedCode',
],
'noCrashOnReadonlyStaticProp' => [
'code' => '<?php
/** @psalm-immutable */
final class C { public int $val = 2; }
final class A {
private static C $prop;
public static function f()
{
self::$prop->val = 1;
}
}
',
'error_message' => 'InaccessibleProperty',
],
'psalm-api with unused private property' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {
private int $b = 0;
}
PHP,
'error_message' => 'UnusedProperty',
],
'psalm-api with final class and unused protected property' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
final class A {
protected int $b = 0;
}
PHP,
'error_message' => 'PossiblyUnusedProperty',
],
'psalm-api with unused private method' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {
private function b(): void {}
}
PHP,
'error_message' => 'UnusedMethod',
],
'psalm-api with final class and unused protected method' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
final class A {
protected function b(): void {}
}
PHP,
'error_message' => 'PossiblyUnusedMethod',
],
'psalm-api with unused class and unused param' => [
'code' => <<<'PHP'
<?php
/** @psalm-api */
class A {
public function b(int $c): void {}
}
PHP,
'error_message' => 'PossiblyUnusedParam',
],
'unused param' => [
'code' => <<<'PHP'
<?php
/** @psalm-suppress UnusedClass */
class A {
public function b(int $c): void {}
}
PHP,
'error_message' => 'PossiblyUnusedParam',
],
'unused param tag' => [
'code' => <<<'PHP'
<?php
/**
* @param string $param
*/
function f(): void {}
PHP,
'error_message' => 'UnusedDocblockParam',
],
];
}
}