diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 6104e067934..a418a52bc22 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1098,6 +1098,15 @@ public function checkPaths(array $paths_to_check): void } } + public function finish(float $start_time, string $psalm_version): void + { + $this->codebase->file_reference_provider->removeDeletedFilesFromReferences(); + + if ($this->project_cache_provider) { + $this->project_cache_provider->processSuccessfulRun($start_time, $psalm_version); + } + } + public function getConfig(): Config { return $this->config; diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index 686aeb92917..5bc637b4624 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -158,6 +158,22 @@ static function ( return false; } + if ($a->type xor $b->type) { + return false; + } + + if ($a->type && $b->type) { + $a_type_start = (int) $a->type->getAttribute('startFilePos'); + $a_type_end = (int) $a->type->getAttribute('endFilePos'); + $b_type_start = (int) $b->type->getAttribute('startFilePos'); + $b_type_end = (int) $b->type->getAttribute('endFilePos'); + if (substr($a_code, $a_type_start, $a_type_end - $a_type_start + 1) + !== substr($b_code, $b_type_start, $b_type_end - $b_type_start + 1) + ) { + return false; + } + } + $body_change = substr($a_code, $a_comments_end, $a_end - $a_comments_end) !== substr($b_code, $b_comments_end, $b_end - $b_comments_end); } else { diff --git a/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php index c91ffcc14ab..8841cf407a8 100644 --- a/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/LanguageServer/Provider/FileStorageCacheProvider.php @@ -19,30 +19,24 @@ public function __construct() { } - public function writeToCache(FileStorage $storage, string $file_contents): void + /** + * @param lowercase-string $file_path + */ + protected function storeInCache(string $file_path, FileStorage $storage): void { - $file_path = strtolower($storage->file_path); $this->cache[$file_path] = $storage; } - public function getLatestFromCache(string $file_path, string $file_contents): ?FileStorage - { - $cached_value = $this->loadFromCache(strtolower($file_path)); - - if (!$cached_value) { - return null; - } - - return $cached_value; - } - public function removeCacheForFile(string $file_path): void { unset($this->cache[strtolower($file_path)]); } - private function loadFromCache(string $file_path): ?FileStorage + /** + * @param lowercase-string $file_path + */ + protected function loadFromCache(string $file_path): ?FileStorage { - return $this->cache[strtolower($file_path)] ?? null; + return $this->cache[$file_path] ?? null; } } diff --git a/src/Psalm/Internal/Provider/FileReferenceProvider.php b/src/Psalm/Internal/Provider/FileReferenceProvider.php index a1e1f2529b4..15950320835 100644 --- a/src/Psalm/Internal/Provider/FileReferenceProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceProvider.php @@ -14,7 +14,6 @@ use function array_merge; use function array_unique; use function explode; -use function file_exists; /** * Used to determine which files reference other files, necessary for using the --diff @@ -164,10 +163,12 @@ class FileReferenceProvider */ private static array $method_param_uses = []; + private FileProvider $file_provider; public ?FileReferenceCacheProvider $cache = null; - public function __construct(?FileReferenceCacheProvider $cache = null) + public function __construct(FileProvider $file_provider, ?FileReferenceCacheProvider $cache = null) { + $this->file_provider = $file_provider; $this->cache = $cache; } @@ -179,7 +180,7 @@ public function getDeletedReferencedFiles(): array if (self::$deleted_files === null) { self::$deleted_files = array_filter( array_keys(self::$file_references), - static fn(string $file_name): bool => !file_exists($file_name) + fn(string $file_name): bool => !$this->file_provider->fileExists($file_name) ); } diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index 6117d9634e3..9fcb7d9d32b 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -64,9 +64,17 @@ public function __construct(Config $config) public function writeToCache(FileStorage $storage, string $file_contents): void { $file_path = strtolower($storage->file_path); - $cache_location = $this->getCacheLocationForPath($file_path, true); $storage->hash = $this->getCacheHash($file_path, $file_contents); + $this->storeInCache($file_path, $storage); + } + + /** + * @param lowercase-string $file_path + */ + protected function storeInCache(string $file_path, FileStorage $storage): void + { + $cache_location = $this->getCacheLocationForPath($file_path, true); $this->cache->saveItem($cache_location, $storage); } @@ -107,7 +115,10 @@ private function getCacheHash(string $_unused_file_path, string $file_contents): return PHP_VERSION_ID >= 8_01_00 ? hash('xxh128', $data) : hash('md4', $data); } - private function loadFromCache(string $file_path): ?FileStorage + /** + * @param lowercase-string $file_path + */ + protected function loadFromCache(string $file_path): ?FileStorage { $storage = $this->cache->getItem($this->getCacheLocationForPath($file_path)); if ($storage instanceof FileStorage) { diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index 57f5f848197..b3a5a2a8abb 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -51,7 +51,7 @@ public function __construct( $parser_cache_provider, $file_storage_cache_provider, ); - $this->file_reference_provider = new FileReferenceProvider($file_reference_cache_provider); + $this->file_reference_provider = new FileReferenceProvider($file_provider, $file_reference_cache_provider); } public static function safeFileGetContents(string $path): string diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index db5ac0ea726..56d7b4fb719 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -792,11 +792,7 @@ public static function finish( } if ($is_full && $start_time) { - $codebase->file_reference_provider->removeDeletedFilesFromReferences(); - - if ($project_analyzer->project_cache_provider) { - $project_analyzer->project_cache_provider->processSuccessfulRun($start_time, PSALM_VERSION); - } + $project_analyzer->finish($start_time, PSALM_VERSION); } if ($error_count diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index 95b66b3a7fc..0efce1c6876 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -19,6 +19,7 @@ use Psalm\Tests\Internal\Provider\ProjectCacheProvider; use Psalm\Tests\TestCase; +use function microtime; use function str_replace; use const DIRECTORY_SEPARATOR; @@ -101,8 +102,10 @@ public function testCacheInteractions( 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); @@ -155,5 +158,46 @@ public function do(): void ], ], ]; + + 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' + [], + ], + ], + ]; } } diff --git a/tests/FileDiffTest.php b/tests/FileDiffTest.php index 5071174c00f..acc84f404bb 100644 --- a/tests/FileDiffTest.php +++ b/tests/FileDiffTest.php @@ -544,6 +544,63 @@ class A { [], [[84, 133]], ], + 'propertyTypeAddition' => [ + ' [ + ' [ + ' [ ' */ + private array $cached_type_coverage = []; + public function __construct() { parent::__construct(Config::getInstance()); @@ -269,10 +272,19 @@ public function setFileMapCache(array $file_maps): void $this->cached_file_maps = $file_maps; } + /** + * @return array + */ + public function getTypeCoverage(): array + { + return $this->cached_type_coverage; + } + /** * @param array $mixed_counts */ public function setTypeCoverage(array $mixed_counts): void { + $this->cached_type_coverage = $mixed_counts; } } diff --git a/tests/Internal/Provider/FileStorageInstanceCacheProvider.php b/tests/Internal/Provider/FileStorageInstanceCacheProvider.php index 7889520c374..b6a732ae1f9 100644 --- a/tests/Internal/Provider/FileStorageInstanceCacheProvider.php +++ b/tests/Internal/Provider/FileStorageInstanceCacheProvider.php @@ -16,30 +16,24 @@ public function __construct() { } - public function writeToCache(FileStorage $storage, string $file_contents): void + /** + * @param lowercase-string $file_path + */ + protected function storeInCache(string $file_path, FileStorage $storage): void { - $file_path = strtolower($storage->file_path); $this->cache[$file_path] = $storage; } - public function getLatestFromCache(string $file_path, string $file_contents): ?FileStorage - { - $cached_value = $this->loadFromCache(strtolower($file_path)); - - if (!$cached_value) { - return null; - } - - return $cached_value; - } - public function removeCacheForFile(string $file_path): void { unset($this->cache[strtolower($file_path)]); } - private function loadFromCache(string $file_path): ?FileStorage + /** + * @param lowercase-string $file_path + */ + protected function loadFromCache(string $file_path): ?FileStorage { - return $this->cache[strtolower($file_path)] ?? null; + return $this->cache[$file_path] ?? null; } } diff --git a/tests/ProjectCheckerTest.php b/tests/ProjectCheckerTest.php index 09ec95f31f1..cce7da9d697 100644 --- a/tests/ProjectCheckerTest.php +++ b/tests/ProjectCheckerTest.php @@ -188,7 +188,7 @@ public function testCheckAfterNoChange(): void $this->assertSame(0, IssueBuffer::getErrorCount()); $this->assertSame( - 'Psalm was able to infer types for 100% of the codebase', + "No files analyzed\nPsalm was able to infer types for 100% of the codebase", $this->project_analyzer->getCodebase()->analyzer->getTypeInferenceSummary( $this->project_analyzer->getCodebase(), ),