file_provider = new FakeFileProvider(); } private function getProjectAnalyzerWithConfig(Config $config): ProjectAnalyzer { $config->setIncludeCollector(new IncludeCollector()); return new ProjectAnalyzer( $config, new Providers( $this->file_provider, new FakeParserCacheProvider() ), new ReportOptions() ); } public function testStringAnalyzerPlugin(): void { $this->expectExceptionMessage('InvalidClass'); $this->expectException(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(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(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(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(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->eventDispatcher->before_file_checks); $this->assertEmpty($codebase->config->eventDispatcher->after_file_checks); $codebase->config->initializePlugins($this->project_analyzer); $this->assertCount(1, $codebase->config->eventDispatcher->before_file_checks); $this->assertCount(1, $codebase->config->eventDispatcher->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(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.phpstub'; $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->eventDispatcher->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 * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint */ public static function afterCodebasePopulated(AfterCodebasePopulatedEvent $event) { } }; $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->eventDispatcher->after_codebase_populated ); } public function testAfterMethodCallAnalysisLegacyHookIsLoaded(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $hook = new class implements LegacyAfterMethodCallAnalysisInterface { public static function afterMethodCallAnalysis( Expr $expr, string $method_id, string $appearing_method_id, string $declaring_method_id, Context $context, StatementsSource $statements_source, Codebase $codebase, array &$file_replacements = [], Union &$return_type_candidate = null ): void { } }; $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; (new PluginRegistrationSocket($config, $codebase))->registerHooksFromClass(get_class($hook)); $this->assertTrue($this->project_analyzer->getCodebase()->config->eventDispatcher->hasAfterMethodCallAnalysisHandlers()); } public function testAfterClassLikeAnalysisLegacyHookIsLoaded(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML( dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, ' ' ) ); $hook = new class implements LegacyAfterClassLikeVisitInterface { /** * @return void * @phpcsSuppress SlevomatCodingStandard.TypeHints.ReturnTypeHint */ public static function afterClassLikeVisit( ClassLike $stmt, ClassLikeStorage $storage, FileSource $statements_source, Codebase $codebase, array &$file_replacements = [] ) { } }; $codebase = $this->project_analyzer->getCodebase(); $config = $codebase->config; (new PluginRegistrationSocket($config, $codebase))->registerHooksFromClass(get_class($hook)); $this->assertTrue($this->project_analyzer->getCodebase()->config->eventDispatcher->hasAfterClassLikeVisitHandlers()); } 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(TSqlSelectString::class, $type); } } public function testPropertyProviderHooksInvalidAssignment(): void { $this->expectExceptionMessage('InvalidPropertyAssignmentValue'); $this->expectException(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(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(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 = Report::TYPE_JSON; $this->project_analyzer->check('tests/fixtures/DummyProject', true); ob_start(); IssueBuffer::finish($this->project_analyzer, true, microtime(true)); ob_end_clean(); } public function testPluginFilenameCanBeAbsolute(): void { /** @var non-empty-string $xml */ $xml = sprintf( ' ', __DIR__ . '/../..' ); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, $xml) ); $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'); /** @var non-empty-string $xml */ $xml = sprintf( ' ', __DIR__ . '/..' ); $this->project_analyzer = $this->getProjectAnalyzerWithConfig( TestConfig::loadFromXML(dirname(__DIR__, 2) . DIRECTORY_SEPARATOR, $xml) ); $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(AfterEveryFunctionCallAnalysisEvent $event): void { $function_id = $event->getFunctionId(); /** @psalm-suppress UndefinedInterfaceMethod */ self::$m->check($function_id); } }; $this->project_analyzer->getCodebase()->config->initializePlugins($this->project_analyzer); $this->project_analyzer->getCodebase()->config->eventDispatcher->after_every_function_checks[] = get_class($plugin); $file_path = getcwd() . '/src/somefile.php'; $this->addFile( $file_path, 'analyzeFile($file_path, new Context()); } public function testRemoveTaints(): 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, ' [ "safe_key" => $_GET["input"], ], ]; output($build);' ); $this->project_analyzer->trackTaintedInputs(); $this->analyzeFile($file_path, new Context()); $this->addFile( $file_path, ' [ "safe_key" => $_GET["input"], "a" => $_GET["input"], ], ]; output($build);' ); $this->project_analyzer->trackTaintedInputs(); $this->expectException(CodeException::class); $this->expectExceptionMessageMatches('/TaintedHtml/'); $this->analyzeFile($file_path, new Context()); } }