From 3e2de9d1526bc80710103dfbd024f6561d931e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Tue, 20 Jun 2023 00:08:32 +0200 Subject: [PATCH 1/3] Implement a way to test cache issues --- .../Internal/Provider/FakeFileProvider.php | 6 + tests/Cache/CacheTest.php | 123 ++++++++++++++++++ tests/Cache/test_base_dir/src/.gitkeep | 0 3 files changed, 129 insertions(+) create mode 100644 tests/Cache/CacheTest.php create mode 100644 tests/Cache/test_base_dir/src/.gitkeep diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php index 642510846d8..756e0d72806 100644 --- a/src/Psalm/Internal/Provider/FakeFileProvider.php +++ b/src/Psalm/Internal/Provider/FakeFileProvider.php @@ -68,6 +68,12 @@ public function registerFile(string $file_path, string $file_contents): void $this->fake_file_times[$file_path] = (int)microtime(true); } + public function deleteFile(string $file_path): void + { + unset($this->fake_files[$file_path]); + unset($this->fake_file_times[$file_path]); + } + /** * @param array $file_extensions * @param null|callable(string):bool $filter diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php new file mode 100644 index 00000000000..4fd5826bc65 --- /dev/null +++ b/tests/Cache/CacheTest.php @@ -0,0 +1,123 @@ +> $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 . 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(); + + $project_analyzer = new ProjectAnalyzer($config, $providers); + $project_analyzer->check($config->base_dir, true); + + $issues = self::normalizeIssueData(IssueBuffer::getIssuesData()); + self::assertSame($interaction['issues'] ?? [], $issues); + } + } + + /** + * @return iterable, + * issues?: array>, + * }>, + * }> + */ + public static function provideCacheInteractions(): iterable + { + } +} diff --git a/tests/Cache/test_base_dir/src/.gitkeep b/tests/Cache/test_base_dir/src/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d From 474f8874f008ab089de0ca6c2650eaa03862b889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Tue, 20 Jun 2023 00:08:53 +0200 Subject: [PATCH 2/3] Add test case for file deletes not invalidating methods --- tests/Cache/CacheTest.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index 4fd5826bc65..95b66b3a7fc 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -119,5 +119,41 @@ public function testCacheInteractions( */ 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', + ], + ], + ], + ], + ]; } } From 1161edfa335bf56265e1f4ed17d8d61ab81cd56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Tue, 20 Jun 2023 00:13:13 +0200 Subject: [PATCH 3/3] Invalidate cached methods when referenced files are deleted --- src/Psalm/Internal/Codebase/Analyzer.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index 6378e484770..ba0209ea6f2 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -687,6 +687,16 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void } } + // This could be optimized by storing method references to files + foreach ($file_reference_provider->getDeletedReferencedFiles() as $deleted_file) { + foreach ($file_reference_provider->getFilesReferencingFile($deleted_file) as $file_referencing_deleted) { + $methods_referencing_deleted = $this->analyzed_methods[$file_referencing_deleted] ?? []; + foreach ($methods_referencing_deleted as $method_referencing_deleted => $_) { + $newly_invalidated_methods[$method_referencing_deleted] = true; + } + } + } + foreach ($newly_invalidated_methods as $method_id => $_) { foreach ($method_references_to_class_members as $i => $_) { unset($method_references_to_class_members[$i][$method_id]);