From fff7c9c05a746ec770d02e16ba8f1dc1efb88b18 Mon Sep 17 00:00:00 2001 From: Dorian Villet Date: Tue, 20 Oct 2026 20:25:48 +0200 Subject: [PATCH] Invalidate the cache when a local checker's code changes The configuration hash covered the registered checkers' class names and configuration, so editing a project's own fixer or sniff kept serving results cached with its previous code until --clear-cache. Hash the source files of checkers living outside vendor/ (with their parents and traits) into it; vendor checkers still change only with their package. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0187H51XtF5pkbEiJDmttSnZ --- src/Caching/FileHashComputer.php | 53 ++++++++++++++++++- .../FileHashComputer/FileHashComputerTest.php | 18 +++++++ .../FileHashComputer/Source/LocalSniff.php | 20 +++++++ .../Source/local_sniff_config.php | 9 ++++ 4 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 tests/ChangedFilesDetector/FileHashComputer/Source/LocalSniff.php create mode 100644 tests/ChangedFilesDetector/FileHashComputer/Source/local_sniff_config.php diff --git a/src/Caching/FileHashComputer.php b/src/Caching/FileHashComputer.php index 559037b3f9..0b5c263f51 100644 --- a/src/Caching/FileHashComputer.php +++ b/src/Caching/FileHashComputer.php @@ -5,6 +5,7 @@ namespace Symplify\EasyCodingStandard\Caching; use Nette\Utils\Json; +use ReflectionClass; use Symplify\EasyCodingStandard\Application\Version\StaticVersionResolver; use Symplify\EasyCodingStandard\Config\ECSConfig; use Symplify\EasyCodingStandard\DependencyInjection\SimpleParameterProvider; @@ -24,8 +25,11 @@ public function computeConfig(string $filePath): string $ecsConfig = new ECSConfig(); $callable($ecsConfig); - // hash the registered checkers and their configuration - $fileHash = sha1(Json::encode($ecsConfig->getCheckerConfiguration())); + // hash the registered checkers, their configuration and the source of the local ones + $checkerConfiguration = $ecsConfig->getCheckerConfiguration(); + $fileHash = sha1( + Json::encode($checkerConfiguration) . $this->hashLocalCheckerFiles(array_keys($checkerConfiguration)) + ); return sha1($fileHash . SimpleParameterProvider::hash() . StaticVersionResolver::PACKAGE_VERSION); } @@ -39,4 +43,49 @@ public function compute(string $filePath): string return $fileHash; } + + /** + * A checker installed in vendor/ only changes with its package, but a project's own checker can change while its + * class name and configuration stay the same, so its code has to be part of the hash. + * + * @param array $checkerClasses + */ + private function hashLocalCheckerFiles(array $checkerClasses): string + { + $fileHashes = []; + foreach ($checkerClasses as $checkerClass) { + foreach ($this->resolveClassFileNames(new ReflectionClass($checkerClass)) as $fileName) { + if (str_contains($fileName, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) { + continue; + } + + $fileHashes[$fileName] = $this->compute($fileName); + } + } + + // contents only, so that the same project checked out elsewhere keeps its cache + $fileHashes = array_values($fileHashes); + sort($fileHashes); + + return sha1(implode('', $fileHashes)); + } + + /** + * @param ReflectionClass $reflectionClass + * @return string[] + */ + private function resolveClassFileNames(ReflectionClass $reflectionClass): array + { + $fileNames = []; + do { + $fileNames[] = $reflectionClass->getFileName(); + foreach ($reflectionClass->getTraits() as $traitReflectionClass) { + $fileNames[] = $traitReflectionClass->getFileName(); + } + + $reflectionClass = $reflectionClass->getParentClass(); + } while ($reflectionClass instanceof ReflectionClass); + + return array_filter($fileNames, is_string(...)); + } } diff --git a/tests/ChangedFilesDetector/FileHashComputer/FileHashComputerTest.php b/tests/ChangedFilesDetector/FileHashComputer/FileHashComputerTest.php index 4dd304bf9c..7c89584b76 100644 --- a/tests/ChangedFilesDetector/FileHashComputer/FileHashComputerTest.php +++ b/tests/ChangedFilesDetector/FileHashComputer/FileHashComputerTest.php @@ -47,6 +47,24 @@ public function testInvalidateCacheOnConfigurationChange(): void $this->filesystem->remove(self::INCLUDED_CONFIG_FILE); } + public function testInvalidateCacheOnLocalCheckerChange(): void + { + $localSniffFile = __DIR__ . '/Source/LocalSniff.php'; + $originalContents = $this->filesystem->readFile($localSniffFile); + + $originalHash = $this->fileHashComputer->computeConfig(__DIR__ . '/Source/local_sniff_config.php'); + + try { + $this->filesystem->dumpFile($localSniffFile, $originalContents . "\n// changed\n"); + + $changedHash = $this->fileHashComputer->computeConfig(__DIR__ . '/Source/local_sniff_config.php'); + } finally { + $this->filesystem->dumpFile($localSniffFile, $originalContents); + } + + $this->assertNotSame($originalHash, $changedHash); + } + public function testPhpFileHash(): void { $fileOne = __DIR__ . '/Source/SomeScannedClass.php'; diff --git a/tests/ChangedFilesDetector/FileHashComputer/Source/LocalSniff.php b/tests/ChangedFilesDetector/FileHashComputer/Source/LocalSniff.php new file mode 100644 index 0000000000..cad44998b6 --- /dev/null +++ b/tests/ChangedFilesDetector/FileHashComputer/Source/LocalSniff.php @@ -0,0 +1,20 @@ +withRules([LocalSniff::class]);