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]);