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 $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 $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, '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, '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 */ public function providerValidCodeParse(): array { return [ 'magicCall' => [ 'code' => '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' => 'foo(); (new B)->foo();', ], 'usedInterfaceMethod' => [ 'code' => 'foo();', ], 'constructorIsUsed' => [ 'code' => 'foo(); } private function foo() : void {} } $a = new A(); echo (bool) $a;', ], 'everythingUsed' => [ 'code' => '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' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'foo(); } takesA(new B);', ], 'usedMethodInTryCatch' => [ 'code' => 'getC(); foreach ([1, 2, 3] as $_) { try { $c->foo(); } catch (Exception $e) {} } } } (new B)->bar();', ], 'suppressPrivateUnusedMethod' => [ 'code' => ' [ 'code' => 'inner(); } abstract protected function inner(): void; } class MyFooBar extends Foobar { protected function inner(): void { // Do nothing } } $myFooBar = new MyFooBar(); $myFooBar->doIt();', ], 'methodUsedAsCallable' => [ 'code' => ' [ 'code' => 'foo; $a->bar(); } foo(new B());', ], 'protectedPropertyOverriddenDownstream' => [ 'code' => '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' => ' [ 'code' => '|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' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'produceFoo(); continue; } } return $foo; }', ], 'suppressUnusedMethod' => [ 'code' => ' [ 'code' => ' [ 'code' => 'foo("COUNT{$s}"); }', ], 'usedFunctioninMethodCallName' => [ 'code' => '{"execute" . ucfirst($action)}($request); } } (new Foo)->bar("request");', ], 'usedMethodCallForExternalMutationFreeClass' => [ 'code' => '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' => ' $arr */ function inc(array $arr) : array { $arr[strlen("hello")]++; return $arr; }', ], 'pureFunctionUsesMethodBeforeReturning' => [ 'code' => 'count = $count; } public function increment() : void { $this->count++; } } /** @psalm-pure */ function makesACounter(int $i) : Counter { $c = new Counter($i); $c->increment(); return $c; }', ], 'usedUsort' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $type * @psalm-return T */ function createFoo($type): FooBase { return new $type(); } class Foo extends FooBase {} createFoo(Foo::class)->baz();', ], 'usedMethodReferencedByString' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' */ public static $foo = []; /** * @param array $map */ public function bar(array $map) : void { self::$foo += $map; } } (new References)->bar(["a" => "b"]);', ], 'promotedPropertyIsUsed' => [ 'code' => 'id; echo $test->name;', ], 'unusedNoReturnFunctionCall' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'create(false)?->test(); $exception = new \Exception(); throw ($exception->getPrevious() ?? $exception);', ], 'publicPropertyReadInFile' => [ 'code' => 'a = "hello"; } } $foo = new A(); echo $foo->a;', ], 'publicPropertyReadInMethod' => [ 'code' => 'a === "goodbye") {} } } (new B)->foo(new A());', ], 'privatePropertyReadInMethod' => [ 'code' => 'a = "hello"; } public function emitA(): void { echo $this->a; } } (new A())->emitA();', ], 'fluentMethodsAllowed' => [ 'code' => 'foo()->bar();', ], 'unusedInterfaceReturnValueWithImplementingClassSuppressed' => [ 'code' => 'work(); } f(new Worker());', ], 'interfaceReturnValueWithImplementingAndAbstractClass' => [ 'code' => 'work(); } f(new Worker()); f(new AnotherWorker());', ], 'methodReturnValueUsedInThrow' => [ 'code' => 'foo(); ', ], 'staticMethodReturnValueUsedInThrow' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => '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' => 'validate(); ', ], '__halt_compiler_no_usage_check' => [ 'code' => ' [ 'code' => 'bar[$a->foo] = "bar"; print_r($a->bar);', ], 'psalm-api with unused class' => [ 'code' => <<<'PHP' [ 'code' => <<<'PHP' [ 'code' => <<<'PHP' }> */ public function providerInvalidCodeParse(): array { return [ 'unusedClass' => [ 'code' => ' 'UnusedClass', ], 'publicUnusedMethod' => [ 'code' => ' 'PossiblyUnusedMethod', ], 'possiblyUnusedParam' => [ 'code' => 'foo(4);', 'error_message' => 'PossiblyUnusedParam - src' . DIRECTORY_SEPARATOR . 'somefile.php:4:49 - Param #1 is never referenced in this method', ], 'unusedParam' => [ 'code' => ' 'UnusedParam', ], 'possiblyUnusedProperty' => [ 'code' => ' 'PossiblyUnusedProperty', 'ignored_issues' => ['UnusedVariable'], ], 'possiblyUnusedPropertyWrittenNeverRead' => [ 'code' => 'foo = "bar";', 'error_message' => 'PossiblyUnusedProperty', 'ignored_issues' => ['UnusedVariable'], ], 'possiblyUnusedPropertyWithArrayWrittenNeverRead' => [ 'code' => ' */ public array $foo = []; } $a = new A(); $a->foo[] = "bar";', 'error_message' => 'PossiblyUnusedProperty', 'ignored_issues' => ['UnusedVariable'], ], 'unusedProperty' => [ 'code' => ' 'UnusedProperty', 'ignored_issues' => ['UnusedVariable'], ], 'privateUnusedMethod' => [ 'code' => ' 'UnusedMethod', ], 'unevaluatedCode' => [ 'code' => ' 'UnevaluatedCode', ], 'unusedTraitMethodInParent' => [ 'code' => 'foo(); } takesA(new B);', 'error_message' => 'PossiblyUnusedMethod', ], 'unusedRecursivelyUsedMethod' => [ 'code' => 'foo(); } } public function bar() : void {} } (new C)->bar();', 'error_message' => 'PossiblyUnusedMethod', ], 'unusedRecursivelyUsedStaticMethod' => [ 'code' => 'bar();', 'error_message' => 'PossiblyUnusedMethod', ], 'unusedFunctionCall' => [ 'code' => ' 'UnusedFunctionCall', ], 'unusedMethodCallSimple' => [ 'code' => 'foo = $foo; } public function getFoo() : string { return $this->foo; } } $a = new A("hello"); $a->getFoo();', 'error_message' => 'UnusedMethodCall', ], 'propertyOverriddenDownstreamAndNotUsed' => [ 'code' => ' 'PossiblyUnusedProperty', ], 'propertyUsedOnlyInConstructor' => [ 'code' => 'used = 4; $this->unused = 4; self::$staticUnused = 4; } public function handle(): void { $this->used++; } } (new A())->handle();', 'error_message' => 'UnusedProperty', ], 'unusedMethodCallForExternalMutationFreeClass' => [ 'code' => 'foo = $foo; } public function setFoo(string $foo) : void { $this->foo = $foo; } } function foo() : void { (new A("hello"))->setFoo("goodbye"); }', 'error_message' => 'UnusedMethodCall', ], 'unusedMethodCallForGeneratingMethod' => [ 'code' => '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' => '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' => 'modify("+1 day"); }', 'error_message' => 'UnusedMethodCall', ], 'unusedClassReferencesItself' => [ 'code' => ' 'UnusedClass', ], 'returnInBothIfConditions' => [ 'code' => ' 'UnevaluatedCode', ], 'unevaluatedCodeAfterReturnInFinally' => [ 'code' => ' 'UnevaluatedCode', ], 'UnusedFunctionCallWithOptionalByReferenceParameter' => [ 'code' => ' 'UnusedFunctionCall', ], 'UnusedFunctionCallWithOptionalByReferenceParameterV2' => [ 'code' => ' 'UnusedFunctionCall', ], 'propertyWrittenButNotRead' => [ 'code' => 'a = "hello"; $this->b = "world"; } } $foo = new A(); echo $foo->a;', 'error_message' => 'PossiblyUnusedProperty', ], 'unusedInterfaceReturnValue' => [ 'code' => 'work(); }', 'error_message' => 'PossiblyUnusedReturnValue', ], 'unusedInterfaceReturnValueWithImplementingClass' => [ 'code' => 'work(); } f(new Worker());', 'error_message' => 'PossiblyUnusedReturnValue', ], 'interfaceWithImplementingClassMethodUnused' => [ 'code' => ' 'PossiblyUnusedMethod', ], 'UnusedFunctionInDoubleConditional' => [ 'code' => ' 'UnusedFunctionCall', ], 'functionNeverUnevaluatedCode' => [ 'code' => ' 'UnevaluatedCode', ], 'methodNeverUnevaluatedCode' => [ 'code' => 'neverReturns(); echo "hello"; } } ', 'error_message' => 'UnevaluatedCode', ], 'exitNeverUnevaluatedCode' => [ 'code' => ' 'UnevaluatedCode', ], 'exitInlineHtml' => [ 'code' => 'foo ', 'error_message' => 'UnevaluatedCode', ], 'noCrashOnReadonlyStaticProp' => [ 'code' => 'val = 1; } } ', 'error_message' => 'InaccessibleProperty', ], 'psalm-api with unused private property' => [ 'code' => <<<'PHP' 'UnusedProperty', ], 'psalm-api with final class and unused protected property' => [ 'code' => <<<'PHP' 'PossiblyUnusedProperty', ], 'psalm-api with unused private method' => [ 'code' => <<<'PHP' 'UnusedMethod', ], 'psalm-api with final class and unused protected method' => [ 'code' => <<<'PHP' 'PossiblyUnusedMethod', ], 'psalm-api with unused class and unused param' => [ 'code' => <<<'PHP' 'PossiblyUnusedParam', ], 'unused param' => [ 'code' => <<<'PHP' 'PossiblyUnusedParam', ], ]; } }