2017-02-08 06:28:26 +01:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Tests;
|
|
|
|
|
2018-01-29 05:41:11 +01:00
|
|
|
use Psalm\Config;
|
2017-07-25 23:04:58 +02:00
|
|
|
use Psalm\Context;
|
2019-03-23 19:27:54 +01:00
|
|
|
use Psalm\Internal\Analyzer\FileAnalyzer;
|
2018-11-12 16:57:05 +01:00
|
|
|
use Psalm\Tests\Internal\Provider;
|
2017-02-08 06:28:26 +01:00
|
|
|
|
2017-04-25 05:45:02 +02:00
|
|
|
class UnusedCodeTest extends TestCase
|
2017-02-08 06:28:26 +01:00
|
|
|
{
|
2018-11-06 03:57:36 +01:00
|
|
|
/** @var \Psalm\Internal\Analyzer\ProjectAnalyzer */
|
2018-11-11 18:01:14 +01:00
|
|
|
protected $project_analyzer;
|
2017-02-08 06:28:26 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return void
|
|
|
|
*/
|
2019-05-17 00:36:36 +02:00
|
|
|
public function setUp() : void
|
2017-02-08 06:28:26 +01:00
|
|
|
{
|
2018-11-06 03:57:36 +01:00
|
|
|
FileAnalyzer::clearCache();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
|
|
$this->file_provider = new Provider\FakeFileProvider();
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer(
|
2018-01-21 16:22:04 +01:00
|
|
|
new TestConfig(),
|
2018-11-06 03:57:36 +01:00
|
|
|
new \Psalm\Internal\Provider\Providers(
|
2018-09-28 22:18:45 +02:00
|
|
|
$this->file_provider,
|
|
|
|
new Provider\FakeParserCacheProvider()
|
|
|
|
)
|
2017-07-25 22:11:02 +02:00
|
|
|
);
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer->getCodebase()->reportUnusedCode();
|
2019-02-07 21:27:43 +01:00
|
|
|
$this->project_analyzer->setPhpVersion('7.3');
|
2017-02-08 06:28:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-06 03:57:36 +01:00
|
|
|
* @dataProvider providerValidCodeParse
|
2017-07-25 23:04:58 +02:00
|
|
|
*
|
|
|
|
* @param string $code
|
|
|
|
* @param array<string> $error_levels
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
2018-01-29 18:13:44 +01:00
|
|
|
public function testValidCode($code, array $error_levels = [])
|
2017-07-25 23:04:58 +02:00
|
|
|
{
|
2018-07-13 23:44:50 +02:00
|
|
|
$test_name = $this->getTestName();
|
2019-06-27 16:17:11 +02:00
|
|
|
if (\strpos($test_name, 'SKIPPED-') !== false) {
|
2017-07-25 23:04:58 +02:00
|
|
|
$this->markTestSkipped('Skipped due to a bug.');
|
|
|
|
}
|
|
|
|
|
2018-01-21 16:22:04 +01:00
|
|
|
$file_path = self::$src_dir_path . 'somefile.php';
|
2017-07-25 23:04:58 +02:00
|
|
|
|
|
|
|
$this->addFile(
|
2018-01-21 16:22:04 +01:00
|
|
|
$file_path,
|
2017-07-25 23:04:58 +02:00
|
|
|
$code
|
|
|
|
);
|
|
|
|
|
2018-01-29 18:13:44 +01:00
|
|
|
foreach ($error_levels as $error_level) {
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
2018-01-29 18:13:44 +01:00
|
|
|
}
|
|
|
|
|
2020-03-28 21:30:56 +01:00
|
|
|
$this->analyzeFile($file_path, new Context(), false);
|
2018-02-17 23:45:30 +01:00
|
|
|
|
2019-12-02 21:24:01 +01:00
|
|
|
$this->project_analyzer->consolidateAnalyzedData();
|
2019-08-18 22:59:56 +02:00
|
|
|
|
|
|
|
\Psalm\IssueBuffer::processUnusedSuppressions($this->project_analyzer->getCodebase()->file_provider);
|
2017-07-25 23:04:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2018-11-06 03:57:36 +01:00
|
|
|
* @dataProvider providerInvalidCodeParse
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-04-25 05:45:02 +02:00
|
|
|
* @param string $code
|
|
|
|
* @param string $error_message
|
2018-01-29 05:41:11 +01:00
|
|
|
* @param array<string> $error_levels
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-08 06:28:26 +01:00
|
|
|
* @return void
|
|
|
|
*/
|
2018-01-29 05:41:11 +01:00
|
|
|
public function testInvalidCode($code, $error_message, $error_levels = [])
|
2017-02-08 06:28:26 +01:00
|
|
|
{
|
2019-06-27 16:17:11 +02:00
|
|
|
if (\strpos($this->getTestName(), 'SKIPPED-') !== false) {
|
2017-07-25 23:04:58 +02:00
|
|
|
$this->markTestSkipped();
|
|
|
|
}
|
|
|
|
|
2019-02-23 22:22:39 +01:00
|
|
|
$this->expectException(\Psalm\Exception\CodeException::class);
|
2019-06-27 16:17:11 +02:00
|
|
|
$this->expectExceptionMessageRegExp('/\b' . \preg_quote($error_message, '/') . '\b/');
|
2017-02-08 06:28:26 +01:00
|
|
|
|
2018-01-21 16:22:04 +01:00
|
|
|
$file_path = self::$src_dir_path . 'somefile.php';
|
|
|
|
|
2018-01-29 05:41:11 +01:00
|
|
|
foreach ($error_levels as $error_level) {
|
2018-11-11 18:01:14 +01:00
|
|
|
$this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS);
|
2018-01-29 05:41:11 +01:00
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->addFile(
|
2018-01-21 16:22:04 +01:00
|
|
|
$file_path,
|
2017-07-25 22:11:02 +02:00
|
|
|
$code
|
|
|
|
);
|
2017-02-08 06:28:26 +01:00
|
|
|
|
2020-03-28 21:30:56 +01:00
|
|
|
$this->analyzeFile($file_path, new Context(), false);
|
2018-02-17 23:45:30 +01:00
|
|
|
|
2019-12-02 21:24:01 +01:00
|
|
|
$this->project_analyzer->consolidateAnalyzedData();
|
2019-08-18 22:59:56 +02:00
|
|
|
|
|
|
|
\Psalm\IssueBuffer::processUnusedSuppressions($this->project_analyzer->getCodebase()->file_provider);
|
2017-02-08 06:28:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-02-23 22:22:39 +01:00
|
|
|
* @return array<string, array{string}>
|
2017-02-08 06:28:26 +01:00
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
public function providerValidCodeParse()
|
2017-02-08 06:28:26 +01:00
|
|
|
{
|
2017-04-25 05:45:02 +02:00
|
|
|
return [
|
2017-11-11 00:08:17 +01:00
|
|
|
'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]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-11 21:50:45 +01:00
|
|
|
private function modify(string $name, string $value): void {
|
2019-02-22 06:50:41 +01:00
|
|
|
call_user_func([$this, "modify" . $name], $value);
|
2017-11-11 00:08:17 +01:00
|
|
|
}
|
|
|
|
|
2018-01-11 21:50:45 +01:00
|
|
|
public function modifyFoo(string $value): void {
|
2017-11-11 00:08:17 +01:00
|
|
|
$this->value = $value;
|
|
|
|
}
|
2018-01-11 05:29:18 +01:00
|
|
|
|
|
|
|
public function getFoo() : string {
|
|
|
|
return $this->value;
|
|
|
|
}
|
2017-11-11 00:08:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$m = new A();
|
|
|
|
$m->foo("value");
|
2018-01-11 05:29:18 +01:00
|
|
|
$m->modifyFoo("value2");
|
|
|
|
echo $m->getFoo();',
|
2017-11-11 00:08:17 +01:00
|
|
|
],
|
2018-06-10 05:10:42 +02:00
|
|
|
'usedTraitMethodWithExplicitCall' => [
|
2017-12-29 17:26:28 +01:00
|
|
|
'<?php
|
|
|
|
class A {
|
2018-01-11 21:50:45 +01:00
|
|
|
public function foo(): void {
|
2017-12-29 17:26:28 +01:00
|
|
|
echo "parent method";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
trait T {
|
2018-01-11 21:50:45 +01:00
|
|
|
public function foo(): void {
|
2017-12-29 17:26:28 +01:00
|
|
|
echo "trait method";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class B extends A {
|
|
|
|
use T;
|
|
|
|
}
|
|
|
|
|
|
|
|
(new A)->foo();
|
|
|
|
(new B)->foo();',
|
|
|
|
],
|
2017-12-30 01:38:01 +01:00
|
|
|
'usedInterfaceMethod' => [
|
|
|
|
'<?php
|
|
|
|
interface I {
|
2018-01-11 21:50:45 +01:00
|
|
|
public function foo(): void;
|
2017-12-30 01:38:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
class A implements I {
|
2018-01-11 21:50:45 +01:00
|
|
|
public function foo(): void {}
|
2017-12-30 01:38:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
(new A)->foo();',
|
|
|
|
],
|
2018-01-29 05:41:11 +01:00
|
|
|
'constructorIsUsed' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
public function __construct() {
|
|
|
|
$this->foo();
|
|
|
|
}
|
|
|
|
private function foo() : void {}
|
|
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
echo (bool) $a;',
|
|
|
|
],
|
2018-02-17 18:02:31 +01:00
|
|
|
'everythingUsed' => [
|
|
|
|
'<?php
|
|
|
|
interface I {
|
2019-07-29 02:42:35 +02:00
|
|
|
public function foo() : void;
|
2018-02-17 18:02:31 +01:00
|
|
|
}
|
|
|
|
class B implements I {
|
|
|
|
public function foo() : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
class A
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var I
|
|
|
|
*/
|
|
|
|
private $i;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int[] $as
|
|
|
|
*/
|
|
|
|
public function __construct(array $as) {
|
2019-02-13 19:32:19 +01:00
|
|
|
$this->i = new B();
|
2018-02-17 18:02:31 +01:00
|
|
|
|
|
|
|
foreach ($as as $a) {
|
|
|
|
$this->a($a, 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private function a(int $a, int $b): self
|
|
|
|
{
|
|
|
|
$this->v($a, $b);
|
|
|
|
|
|
|
|
$this->i->foo();
|
|
|
|
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function v(int $a, int $b): void
|
|
|
|
{
|
|
|
|
if ($a + $b > 0) {
|
|
|
|
throw new \RuntimeException("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
new A([1, 2, 3]);',
|
|
|
|
],
|
2018-02-27 17:39:26 +01:00
|
|
|
'unusedParamWithUnderscore' => [
|
|
|
|
'<?php
|
|
|
|
function foo(int $_) : void {}
|
|
|
|
|
|
|
|
foo(4);',
|
|
|
|
],
|
|
|
|
'unusedParamWithUnusedPrefix' => [
|
|
|
|
'<?php
|
|
|
|
function foo(int $unusedArg) : void {}
|
|
|
|
|
|
|
|
foo(4);',
|
|
|
|
],
|
2019-08-21 19:12:13 +02:00
|
|
|
'usedFunctionCall' => [
|
|
|
|
'<?php
|
|
|
|
$a = strlen("goodbye");
|
|
|
|
echo $a;',
|
|
|
|
],
|
2018-02-27 17:39:26 +01:00
|
|
|
'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();',
|
|
|
|
],
|
2018-03-07 17:16:56 +01:00
|
|
|
'usedClass' => [
|
|
|
|
'<?php
|
|
|
|
class A { }
|
|
|
|
new A();',
|
|
|
|
],
|
2018-06-10 05:10:42 +02:00
|
|
|
'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();
|
|
|
|
}
|
2019-03-23 19:27:54 +01:00
|
|
|
takesA(new B);',
|
2018-06-10 05:10:42 +02:00
|
|
|
],
|
2018-08-07 19:17:23 +02:00
|
|
|
'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();
|
|
|
|
|
2019-05-02 23:21:02 +02:00
|
|
|
foreach ([1, 2, 3] as $_) {
|
2018-08-07 19:17:23 +02:00
|
|
|
try {
|
|
|
|
$c->foo();
|
|
|
|
} catch (Exception $e) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
(new B)->bar();',
|
|
|
|
],
|
2018-07-12 18:12:28 +02:00
|
|
|
'suppressPrivateUnusedMethod' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress UnusedMethod
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
private function foo() {}
|
|
|
|
}
|
|
|
|
|
|
|
|
new A();',
|
|
|
|
],
|
2018-12-13 23:20:29 +01:00
|
|
|
'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();',
|
|
|
|
],
|
2019-04-17 21:45:40 +02:00
|
|
|
'methodUsedAsCallable' => [
|
|
|
|
'<?php
|
|
|
|
class C {
|
|
|
|
public static function foo() : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
function takesCallable(callable $c) : void {
|
|
|
|
$c();
|
|
|
|
}
|
|
|
|
|
|
|
|
takesCallable([C::class, "foo"]);',
|
|
|
|
],
|
2019-04-17 22:41:35 +02:00
|
|
|
'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());',
|
|
|
|
],
|
2019-04-17 22:55:15 +02:00
|
|
|
'protectedPropertyOverriddenDownstream' => [
|
|
|
|
'<?php
|
|
|
|
|
|
|
|
class C {
|
|
|
|
/** @var int */
|
|
|
|
protected $foo = 1;
|
2019-08-30 22:40:32 +02:00
|
|
|
public function bar() : void {
|
|
|
|
$this->foo = 5;
|
2019-04-17 22:55:15 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class D extends C {
|
|
|
|
protected $foo = 2;
|
|
|
|
}
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
(new D)->bar();',
|
2019-04-17 22:55:15 +02:00
|
|
|
],
|
2019-06-16 00:49:58 +02:00
|
|
|
'usedClassAfterExtensionLoaded' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
public function __construct() {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (extension_loaded("fdsfdsfd")) {
|
|
|
|
new A();
|
|
|
|
}',
|
|
|
|
],
|
2019-06-26 03:46:18 +02:00
|
|
|
'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
|
|
|
|
* @return self
|
|
|
|
*/
|
|
|
|
public function addType(?string $type, array $ids = array())
|
|
|
|
{
|
|
|
|
if ($this->a) {
|
|
|
|
$ids = self::mirror($ids);
|
|
|
|
}
|
|
|
|
$this->_types[$type ?: ""] = new ArrayObject($ids);
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
(new C)->addType(null);',
|
2019-06-26 03:46:18 +02:00
|
|
|
],
|
2019-06-27 16:17:11 +02:00
|
|
|
'usedMethodAfterClassExists' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
public static function bar() : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (class_exists(A::class)) {
|
|
|
|
A::bar();
|
2019-07-05 22:24:00 +02:00
|
|
|
}',
|
2019-06-27 16:17:11 +02:00
|
|
|
],
|
2019-07-31 23:35:56 +02:00
|
|
|
'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;
|
|
|
|
|
2019-07-31 23:42:01 +02:00
|
|
|
continue;
|
2019-07-31 23:35:56 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}',
|
|
|
|
],
|
2019-08-18 22:59:56 +02:00
|
|
|
'suppressUnusedMethod' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress UnusedMethod
|
|
|
|
*/
|
|
|
|
public function foo() : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
new A();'
|
|
|
|
],
|
2019-08-23 04:06:22 +02:00
|
|
|
'usedFunctionInCall' => [
|
|
|
|
'<?php
|
|
|
|
function fooBar(): void {}
|
|
|
|
|
|
|
|
$foo = "foo";
|
|
|
|
$bar = "bar";
|
|
|
|
|
|
|
|
($foo . ucfirst($bar))();',
|
|
|
|
],
|
2019-08-23 16:59:59 +02:00
|
|
|
'usedParamInUnknownMethodConcat' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @psalm-suppress MixedMethodCall
|
|
|
|
*/
|
|
|
|
function foo(string $s, object $o) : void {
|
|
|
|
$o->foo("COUNT{$s}");
|
|
|
|
}'
|
|
|
|
],
|
2019-08-26 06:47:46 +02:00
|
|
|
'usedFunctioninMethodCallName' => [
|
|
|
|
'<?php
|
|
|
|
class Foo {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress MixedArgument
|
|
|
|
*/
|
2020-03-06 03:24:08 +01:00
|
|
|
public function bar(string $request): void {
|
2019-08-26 06:47:46 +02:00
|
|
|
/** @var mixed $action */
|
|
|
|
$action = "";
|
|
|
|
$this->{"execute" . ucfirst($action)}($request);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-06 03:24:08 +01:00
|
|
|
(new Foo)->bar("request");'
|
2019-08-26 06:47:46 +02:00
|
|
|
],
|
2019-08-30 23:26:55 +02:00
|
|
|
'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");',
|
|
|
|
],
|
2019-08-31 00:06:45 +02:00
|
|
|
'functionUsedAsArrayKeyInc' => [
|
|
|
|
'<?php
|
|
|
|
/** @param array<int, int> $arr */
|
|
|
|
function inc(array $arr) : array {
|
|
|
|
$arr[strlen("hello")]++;
|
|
|
|
return $arr;
|
|
|
|
}'
|
|
|
|
],
|
2019-08-31 16:02:11 +02:00
|
|
|
'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;
|
|
|
|
}',
|
|
|
|
],
|
2019-09-09 18:11:04 +02:00
|
|
|
'usedUsort' => [
|
|
|
|
'<?php
|
|
|
|
/** @param string[] $arr */
|
|
|
|
function foo(array $arr) : array {
|
|
|
|
usort($arr, "strnatcasecmp");
|
|
|
|
return $arr;
|
|
|
|
}'
|
|
|
|
],
|
2019-09-10 03:28:56 +02:00
|
|
|
'allowArrayMapWithClosure' => [
|
|
|
|
'<?php
|
|
|
|
$a = [1, 2, 3];
|
|
|
|
|
|
|
|
array_map(function($i) { echo $i;}, $a);'
|
|
|
|
],
|
2019-12-11 16:13:22 +01:00
|
|
|
'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;
|
|
|
|
}'
|
|
|
|
],
|
2019-12-11 18:06:10 +01:00
|
|
|
'usedFunctionCallInsideSwitchWithTernary' => [
|
|
|
|
'<?php
|
|
|
|
function getArg(string $method) : void {
|
|
|
|
switch (strtolower($method ?: "")) {
|
|
|
|
case "post":
|
|
|
|
break;
|
|
|
|
|
|
|
|
case "get":
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}'
|
2019-12-31 13:56:35 +01:00
|
|
|
],
|
|
|
|
'ignoreSerializerSerialize' => [
|
|
|
|
'<?php
|
|
|
|
class Foo implements Serializable {
|
|
|
|
public function serialize() : string {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
public function unserialize($_serialized) : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
new Foo();'
|
|
|
|
],
|
2020-01-02 00:33:12 +01:00
|
|
|
'useIteratorMethodsWhenCallingForeach' => [
|
|
|
|
'<?php
|
|
|
|
/** @psalm-suppress UnimplementedInterfaceMethod */
|
|
|
|
class IterableResult implements \Iterator {
|
|
|
|
public function current() {
|
|
|
|
return $this->current;
|
|
|
|
}
|
2020-04-27 16:03:16 +02:00
|
|
|
|
|
|
|
public function key() {
|
|
|
|
return 5;
|
|
|
|
}
|
2020-01-02 00:33:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$items = new IterableResult();
|
|
|
|
|
|
|
|
foreach ($items as $_item) {}'
|
|
|
|
],
|
2020-03-12 16:42:01 +01:00
|
|
|
'usedThroughNewClassStringOfBase' => [
|
|
|
|
'<?php
|
|
|
|
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();'
|
|
|
|
],
|
2020-06-20 00:02:39 +02:00
|
|
|
'usedMethodReferencedByString' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
static function b(): void {}
|
|
|
|
}
|
|
|
|
$methodRef = "A::b";
|
|
|
|
$methodRef();',
|
|
|
|
],
|
|
|
|
'usedMethodReferencedByStringWithLeadingBackslash' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
static function b(): void {}
|
|
|
|
}
|
|
|
|
$methodRef = "\A::b";
|
|
|
|
$methodRef();',
|
|
|
|
],
|
2020-07-10 22:14:24 +02:00
|
|
|
'doArrayIncrement' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param list<int> $keys
|
|
|
|
* @param int $key
|
|
|
|
*/
|
|
|
|
function error2(array $keys, int $key): int
|
|
|
|
{
|
|
|
|
if ($key === 1) {}
|
|
|
|
|
|
|
|
do {
|
|
|
|
$nextKey = $keys[++$key] ?? null;
|
|
|
|
} while ($nextKey === null);
|
|
|
|
|
|
|
|
return $nextKey;
|
|
|
|
}'
|
|
|
|
],
|
2020-07-16 19:44:51 +02:00
|
|
|
'arrayPushFunctionCall' => [
|
|
|
|
'<?php
|
|
|
|
$a = [];
|
|
|
|
|
|
|
|
array_push($a, strlen("hello"));
|
|
|
|
|
|
|
|
echo $a[0];'
|
|
|
|
],
|
2017-04-25 05:45:02 +02:00
|
|
|
];
|
2017-02-08 06:28:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-02-23 22:22:39 +01:00
|
|
|
* @return array<string,array{string,error_message:string}>
|
2017-02-08 06:28:26 +01:00
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
public function providerInvalidCodeParse()
|
2017-02-08 06:28:26 +01:00
|
|
|
{
|
2017-04-25 05:45:02 +02:00
|
|
|
return [
|
|
|
|
'unusedClass' => [
|
|
|
|
'<?php
|
|
|
|
class A { }',
|
2017-05-27 02:05:57 +02:00
|
|
|
'error_message' => 'UnusedClass',
|
2017-04-25 05:45:02 +02:00
|
|
|
],
|
|
|
|
'publicUnusedMethod' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/** @return void */
|
|
|
|
public function foo() {}
|
|
|
|
}
|
2017-05-04 20:25:58 +02:00
|
|
|
|
2017-04-25 05:45:02 +02:00
|
|
|
new A();',
|
2017-05-27 02:05:57 +02:00
|
|
|
'error_message' => 'PossiblyUnusedMethod',
|
2017-04-25 05:45:02 +02:00
|
|
|
],
|
2018-01-22 05:48:58 +01:00
|
|
|
'possiblyUnusedParam' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/** @return void */
|
|
|
|
public function foo(int $i) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
(new A)->foo(4);',
|
2019-06-27 16:17:11 +02:00
|
|
|
'error_message' => 'PossiblyUnusedParam - src' . \DIRECTORY_SEPARATOR
|
2019-05-13 22:01:41 +02:00
|
|
|
. 'somefile.php:4:49 - Param #1 is never referenced in this method',
|
2018-01-22 05:48:58 +01:00
|
|
|
],
|
|
|
|
'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',
|
2018-01-29 05:41:11 +01:00
|
|
|
'error_levels' => ['UnusedVariable'],
|
2018-01-22 05:48:58 +01:00
|
|
|
],
|
|
|
|
'unusedProperty' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/** @var string */
|
|
|
|
private $foo = "hello";
|
|
|
|
}
|
|
|
|
|
|
|
|
$a = new A();',
|
|
|
|
'error_message' => 'UnusedProperty',
|
2018-01-29 05:41:11 +01:00
|
|
|
'error_levels' => ['UnusedVariable'],
|
2018-01-22 05:48:58 +01:00
|
|
|
],
|
2017-04-25 05:45:02 +02:00
|
|
|
'privateUnusedMethod' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/** @return void */
|
|
|
|
private function foo() {}
|
|
|
|
}
|
2017-05-04 20:25:58 +02:00
|
|
|
|
2017-04-25 05:45:02 +02:00
|
|
|
new A();',
|
2017-05-27 02:05:57 +02:00
|
|
|
'error_message' => 'UnusedMethod',
|
|
|
|
],
|
2017-12-30 16:54:01 +01:00
|
|
|
'unevaluatedCode' => [
|
|
|
|
'<?php
|
2018-01-11 21:50:45 +01:00
|
|
|
function foo(): void {
|
2017-12-30 16:54:01 +01:00
|
|
|
return;
|
|
|
|
$a = "foo";
|
|
|
|
}',
|
|
|
|
'error_message' => 'UnevaluatedCode',
|
|
|
|
],
|
2018-06-10 05:10:42 +02:00
|
|
|
'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',
|
|
|
|
],
|
2018-06-19 22:14:51 +02:00
|
|
|
'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',
|
|
|
|
],
|
2019-08-21 19:12:13 +02:00
|
|
|
'unusedFunctionCall' => [
|
|
|
|
'<?php
|
|
|
|
strlen("goodbye");',
|
|
|
|
'error_message' => 'UnusedFunctionCall',
|
|
|
|
],
|
2020-07-17 00:14:15 +02:00
|
|
|
'unusedMethodCallSimple' => [
|
2019-08-30 22:40:32 +02:00
|
|
|
'<?php
|
2020-07-16 22:19:29 +02:00
|
|
|
final class A {
|
2019-08-30 22:40:32 +02:00
|
|
|
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',
|
|
|
|
],
|
2019-04-17 22:41:35 +02:00
|
|
|
'propertyOverriddenDownstreamAndNotUsed' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/** @var string */
|
|
|
|
public $foo = "hello";
|
|
|
|
}
|
|
|
|
|
|
|
|
class B extends A {
|
|
|
|
/** @var string */
|
|
|
|
public $foo = "goodbye";
|
|
|
|
}
|
|
|
|
|
|
|
|
new B();',
|
|
|
|
'error_message' => 'PossiblyUnusedProperty',
|
|
|
|
],
|
2019-07-22 05:29:16 +02:00
|
|
|
'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',
|
|
|
|
],
|
2019-08-30 23:26:55 +02:00
|
|
|
'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',
|
|
|
|
],
|
2019-08-30 23:34:58 +02:00
|
|
|
'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',
|
|
|
|
],
|
2019-08-31 06:47:12 +02:00
|
|
|
'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',
|
2019-09-03 19:49:15 +02:00
|
|
|
],
|
2020-03-09 23:55:35 +01:00
|
|
|
'dateTimeImmutable' => [
|
2019-09-03 19:49:15 +02:00
|
|
|
'<?php
|
|
|
|
function foo(DateTimeImmutable $dt) : void {
|
|
|
|
$dt->modify("+1 day");
|
|
|
|
}',
|
|
|
|
'error_message' => 'UnusedMethodCall',
|
|
|
|
],
|
2019-12-16 17:46:10 +01:00
|
|
|
'unusedClassReferencesItself' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
class AChild extends A {
|
|
|
|
public function __construct() {
|
|
|
|
self::foo();
|
|
|
|
}
|
|
|
|
public static function foo() : void {}
|
|
|
|
}',
|
|
|
|
'error_message' => 'UnusedClass',
|
|
|
|
],
|
2020-03-15 19:31:41 +01:00
|
|
|
'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',
|
|
|
|
],
|
2017-04-25 05:45:02 +02:00
|
|
|
];
|
2017-02-08 06:28:26 +01:00
|
|
|
}
|
|
|
|
}
|