> $issue_data * @return array> */ private static function normalizeIssueData(array $issue_data): array { $return = []; foreach ($issue_data as $issue_data_per_file) { foreach ($issue_data_per_file as $one_issue_data) { $file_name = str_replace(DIRECTORY_SEPARATOR, '/', $one_issue_data->file_name); $return[$file_name][] = $one_issue_data->type . ': ' . $one_issue_data->message; } } return $return; } /** * @param list, * issues?: array>, * }> $interactions * @dataProvider provideCacheInteractions */ public function testCacheInteractions( array $interactions ): void { $config = Config::loadFromXML( __DIR__ . DIRECTORY_SEPARATOR . 'test_base_dir', <<<'XML' XML, ); $config->setIncludeCollector(new IncludeCollector()); $file_provider = new FakeFileProvider(); $providers = new Providers( $file_provider, new ParserInstanceCacheProvider(), new FileStorageInstanceCacheProvider(), new ClassLikeStorageInstanceCacheProvider(), new FakeFileReferenceCacheProvider(), new ProjectCacheProvider(), ); foreach ($interactions as $interaction) { foreach ($interaction['files'] as $file_path => $file_contents) { $file_path = $config->base_dir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $file_path); if ($file_contents === null) { $file_provider->deleteFile($file_path); } else { $file_provider->registerFile($file_path, $file_contents); } } RuntimeCaches::clearAll(); $start_time = microtime(true); $project_analyzer = new ProjectAnalyzer($config, $providers); $project_analyzer->check($config->base_dir, true); $project_analyzer->finish($start_time, PSALM_VERSION); $issues = self::normalizeIssueData(IssueBuffer::getIssuesData()); self::assertSame($interaction['issues'] ?? [], $issues); } } /** * @return iterable, * issues?: array>, * }>, * }> */ public static function provideCacheInteractions(): iterable { yield 'deletedFileInvalidatesReferencingMethod' => [ [ [ 'files' => [ 'src/A.php' => <<<'PHP' do(); } } PHP, 'src/B.php' => <<<'PHP' [ 'src/B.php' => null, ], 'issues' => [ 'src/A.php' => [ 'UndefinedClass: Class, interface or enum named B does not exist', ], ], ], ], ]; yield 'classPropertyTypeChangeInvalidatesReferencingMethod' => [ [ [ 'files' => [ 'src/A.php' => <<<'PHP' value; } } PHP, 'src/B.php' => <<<'PHP' [ 'src/A.php' => [ "NullableReturnStatement: The declared return type 'int' for A::foo is not nullable, but the function returns 'int|null'", "InvalidNullableReturnType: The declared return type 'int' for A::foo is not nullable, but 'int|null' contains null", ], ], ], [ 'files' => [ 'src/B.php' => <<<'PHP' [], ], ], ]; yield 'classDocblockChange' => [ [ [ 'files' => [ 'src/A.php' => <<<'PHP' <<<'PHP' foo(1); } } PHP, ], 'issues' => [], ], [ 'files' => [ 'src/A.php' => <<<'PHP' [ 'src/A.php' => [ "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", ], 'src/B.php' => [ "InvalidArgument: Argument 1 of A::foo expects T, but 1 provided", ], ], ], ], ]; yield 'constructorPropertyPromotionChange' => [ [ [ 'files' => [ 'src/A.php' => <<<'PHP' foo; } } PHP, ], 'issues' => [], ], [ 'files' => [ 'src/A.php' => <<<'PHP' foo; } } PHP, ], 'issues' => [ 'src/A.php' => [ "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", "MixedReturnStatement: Could not infer a return type", "MixedInferredReturnType: Could not verify return type 'string' for A::bar", ], ], ], ], ]; } }