file_provider = new Provider\FakeFileProvider(); } private function getProjectAnalyzerWithConfig(Config $config): \Psalm\Internal\Analyzer\ProjectAnalyzer { $config->setIncludeCollector(new IncludeCollector()); return new \Psalm\Internal\Analyzer\ProjectAnalyzer( $config, new \Psalm\Internal\Provider\Providers( $this->file_provider, new Provider\FakeParserCacheProvider() ), new \Psalm\Report\ReportOptions() ); } public function testStringAnalyzerPlugin(): void { $this->expectExceptionMessage('InvalidClass'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testStringAnalyzerPluginWithClassConstant(): void { $this->expectExceptionMessage('InvalidClass'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, ' "Psalm\Internal\Analyzer\ProjectAnalyzer", ]; }' ); $this->analyzeFile($file_path, new Context()); } public function testStringAnalyzerPluginWithClassConstantConcat(): void { $this->expectExceptionMessage('UndefinedMethod'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, ' \Psalm\Internal\Analyzer\ProjectAnalyzer::class . "::foo", ]; }' ); $this->analyzeFile($file_path, new Context()); } public function testEchoAnalyzerPluginWithJustHtml(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, '

This is a header

' ); $this->analyzeFile($file_path, new Context()); } public function testEchoAnalyzerPluginWithUnescapedConcatenatedString(): void { $this->expectExceptionMessage('TypeCoercion'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, '' ); $this->analyzeFile($file_path, new Context()); } public function testEchoAnalyzerPluginWithUnescapedString(): void { $this->expectExceptionMessage('TypeCoercion'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, '' ); $this->analyzeFile($file_path, new Context()); } public function testEchoAnalyzerPluginWithEscapedString(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, ' Some text ' ); $this->analyzeFile($file_path, new Context()); } public function testFileAnalyzerPlugin(): void { require_once __DIR__ . '/Plugin/FilePlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $codebase = $this->project_analyzer->getCodebase(); $this->assertEmpty($codebase->config->before_file_checks); $this->assertEmpty($codebase->config->after_file_checks); $codebase->config->initializePlugins($this->project_analyzer); $this->assertCount(1, $codebase->config->before_file_checks); $this->assertCount(1, $codebase->config->after_file_checks); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); $file_storage = $codebase->file_storage_provider->get($file_path); $this->assertEquals( [ 'before-analysis' => true, 'after-analysis' => true, ], $file_storage->custom_metadata ); } public function testFloatCheckerPlugin(): void { $this->expectExceptionMessage('NoFloatAssignment'); $this->expectException(\Psalm\Exception\CodeException::class); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testFloatCheckerPluginIssueSuppressionByConfig(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testFloatCheckerPluginIssueSuppressionByDocblock(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testInheritedHookHandlersAreCalled(): void { require_once dirname(__DIR__) . '/fixtures/stubs/extending_plugin_entrypoint.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->assertContains( 'ExtendingPlugin', $this->project_analyzer->getCodebase()->config->after_function_checks ); } public function testAfterCodebasePopulatedHookIsLoaded(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $hook = new class implements AfterCodebasePopulatedInterface { /** @return void */ public static function afterCodebasePopulated(Codebase $codebase) { } }; $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; (new PluginRegistrationSocket($config, $codebase))->registerHooksFromClass(get_class($hook)); $this->assertContains( get_class($hook), $this->project_analyzer->getCodebase()->config->after_codebase_populated ); } public function testPropertyProviderHooks(): void { require_once __DIR__ . '/Plugin/PropertyPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'magic_property;' ); $this->analyzeFile($file_path, new Context()); } public function testMethodProviderHooksValidArg(): void { require_once __DIR__ . '/Plugin/MethodPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'magicMethod("hello"); echo strlen($foo->magicMethod("hello")); echo $foo::magicMethod("hello"); echo strlen($foo::magicMethod("hello")); $foo2 = $foo->magicMethod2("test"); $foo2->id(); i($foo2); echo $foo2->id();' ); $this->analyzeFile($file_path, new Context()); } public function testFunctionProviderHooks(): void { require_once __DIR__ . '/Plugin/FunctionPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testSqlStringProviderHooks(): void { require_once __DIR__ . '/Plugin/SqlStringProviderPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, $context); $this->assertTrue(isset($context->vars_in_scope['$a'])); foreach ($context->vars_in_scope['$a']->getAtomicTypes() as $type) { $this->assertInstanceOf(\Psalm\Test\Config\Plugin\Hook\StringProvider\TSqlSelectString::class, $type); } } public function testPropertyProviderHooksInvalidAssignment(): void { $this->expectExceptionMessage('InvalidPropertyAssignmentValue'); $this->expectException(\Psalm\Exception\CodeException::class); require_once __DIR__ . '/Plugin/PropertyPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'magic_property = 5;' ); $this->analyzeFile($file_path, new Context()); } public function testMethodProviderHooksInvalidArg(): void { $this->expectExceptionMessage('InvalidScalarArgument'); $this->expectException(\Psalm\Exception\CodeException::class); require_once __DIR__ . '/Plugin/MethodPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'magicMethod(5));' ); $this->analyzeFile($file_path, new Context()); } public function testFunctionProviderHooksInvalidArg(): void { $this->expectExceptionMessage('InvalidScalarArgument'); $this->expectException(\Psalm\Exception\CodeException::class); require_once __DIR__ . '/Plugin/FunctionPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testAfterAnalysisHooks(): void { require_once __DIR__ . '/Plugin/AfterAnalysisPlugin.php'; $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->assertNotNull($this->project_analyzer->stdout_report_options); $this->project_analyzer->stdout_report_options->format = \Psalm\Report::TYPE_JSON; $this->project_analyzer->check('tests/fixtures/DummyProject', true); ob_start(); \Psalm\IssueBuffer::finish($this->project_analyzer, true, microtime(true)); ob_end_clean(); } public function testPluginFilenameCanBeAbsolute(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, sprintf( ' ', __DIR__ . '/../..' ) ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); } public function testPluginInvalidAbsoluteFilenameThrowsException() : void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('does-not-exist/plugins/StringChecker.php'); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, sprintf( ' ', __DIR__ . '/..' ) ) ); $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); } public function testAfterEveryFunctionPluginIsCalledInAllCases(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $mock = $this->getMockBuilder(\stdClass::class)->setMethods(['check'])->getMock(); $mock->expects($this->exactly(4)) ->method('check') ->withConsecutive( [$this->equalTo('b')], [$this->equalTo('array_map')], [$this->equalTo('fopen')], [$this->equalTo('a')] ); $plugin = new class($mock) implements AfterEveryFunctionCallAnalysisInterface { /** @var MockObject */ private static $m; public function __construct(MockObject $m) { self::$m = $m; } public static function afterEveryFunctionCallAnalysis( FuncCall $expr, string $function_id, Context $context, StatementsSource $statements_source, Codebase $codebase ): void { /** @psalm-suppress UndefinedInterfaceMethod */ self::$m->check($function_id); } }; $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->project_analyzer->getCodebase()->config->after_every_function_checks[] = get_class($plugin); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } }