file_provider = new Provider\FakeFileProvider(); $this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer( new TestConfig(), new \Psalm\Internal\Provider\Providers( $this->file_provider, new Provider\FakeParserCacheProvider() ) ); $this->project_analyzer->getCodebase()->reportUnusedCode(); $this->project_analyzer->setPhpVersion('7.3'); } /** * @dataProvider providerValidCodeParse * * @param string $code * @param array $error_levels * * @return void */ public function testValidCode($code, array $error_levels = []) { $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 ); foreach ($error_levels as $error_level) { $this->project_analyzer->getCodebase()->config->setCustomErrorLevel($error_level, Config::REPORT_SUPPRESS); } $context = new Context(); $context->collect_references = true; $this->analyzeFile($file_path, $context); $this->project_analyzer->checkClassReferences(); } /** * @dataProvider providerInvalidCodeParse * * @param string $code * @param string $error_message * @param array $error_levels * * @return void */ public function testInvalidCode($code, $error_message, $error_levels = []) { if (\strpos($this->getTestName(), 'SKIPPED-') !== false) { $this->markTestSkipped(); } $this->expectException(\Psalm\Exception\CodeException::class); $this->expectExceptionMessageRegExp('/\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 ); $context = new Context(); $context->collect_references = true; $this->analyzeFile($file_path, $context); $this->project_analyzer->checkClassReferences(); } /** * @return array */ public function providerValidCodeParse() { return [ 'magicCall' => [ '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' => [ 'foo(); (new B)->foo();', ], 'usedInterfaceMethod' => [ 'foo();', ], 'constructorIsUsed' => [ 'foo(); } private function foo() : void {} } $a = new A(); echo (bool) $a;', ], 'everythingUsed' => [ 'i = new B(); 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]);', ], 'unusedParamWithUnderscore' => [ ' [ ' [ ' [ ' [ ' [ 'foo(); } takesA(new B);', ], 'usedMethodInTryCatch' => [ 'getC(); foreach ([1, 2, 3] as $_) { try { $c->foo(); } catch (Exception $e) {} } } } (new B)->bar();', ], 'suppressPrivateUnusedMethod' => [ ' [ 'inner(); } abstract protected function inner(): void; } class MyFooBar extends Foobar { protected function inner(): void { // Do nothing } } $myFooBar = new MyFooBar(); $myFooBar->doIt();', ], 'methodUsedAsCallable' => [ ' [ 'foo; $a->bar(); } foo(new B());', ], 'protectedPropertyOverriddenDownstream' => [ 'foo; } } class D extends C { protected $foo = 2; } (new D)->bar();', ], 'usedClassAfterExtensionLoaded' => [ ' [ '|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; } } (new C)->addType(null);', ], 'usedMethodAfterClassExists' => [ ' [ ' [ ' [ 'produceFoo(); continue; } } return $foo; }', ], ]; } /** * @return array */ public function providerInvalidCodeParse() { return [ 'unusedClass' => [ ' 'UnusedClass', ], 'publicUnusedMethod' => [ ' 'PossiblyUnusedMethod', ], 'possiblyUnusedParam' => [ 'foo(4);', 'error_message' => 'PossiblyUnusedParam - src' . \DIRECTORY_SEPARATOR . 'somefile.php:4:49 - Param #1 is never referenced in this method', ], 'unusedParam' => [ ' 'UnusedParam', ], 'possiblyUnusedProperty' => [ ' 'PossiblyUnusedProperty', 'error_levels' => ['UnusedVariable'], ], 'unusedProperty' => [ ' 'UnusedProperty', 'error_levels' => ['UnusedVariable'], ], 'privateUnusedMethod' => [ ' 'UnusedMethod', ], 'unevaluatedCode' => [ ' 'UnevaluatedCode', ], 'unusedTraitMethodInParent' => [ 'foo(); } takesA(new B);', 'error_message' => 'PossiblyUnusedMethod', ], 'unusedRecursivelyUsedMethod' => [ 'foo(); } } public function bar() : void {} } (new C)->bar();', 'error_message' => 'PossiblyUnusedMethod', ], 'unusedRecursivelyUsedStaticMethod' => [ 'bar();', 'error_message' => 'PossiblyUnusedMethod', ], 'propertyOverriddenDownstreamAndNotUsed' => [ ' 'PossiblyUnusedProperty', ], 'propertyUsedOnlyInConstructor' => [ 'used = 4; $this->unused = 4; self::$staticUnused = 4; } public function handle(): void { $this->used++; } } (new A())->handle();', 'error_message' => 'UnusedProperty', ], ]; } }