diff --git a/src/Application/ApplicationFileProcessor.php b/src/Application/ApplicationFileProcessor.php index 6cfd9b99bb6..09a9b1e7b73 100644 --- a/src/Application/ApplicationFileProcessor.php +++ b/src/Application/ApplicationFileProcessor.php @@ -56,6 +56,9 @@ public function __construct( public function run(Configuration $configuration, InputInterface $input): ProcessResult { + // scope the cache to this run's --only / --only-suffix selection before any cache read/write + $this->changedFilesDetector->setActiveScope($configuration->getOnlyRule(), $configuration->getOnlySuffix()); + $filePaths = $this->filesFinder->findFilesInPaths($configuration->getPaths(), $configuration); // no files found @@ -121,6 +124,9 @@ public function processFiles( ?callable $preFileCallback = null, ?callable $postFileCallback = null ): ProcessResult { + // also set here: parallel workers reach processFiles() via WorkerCommand, bypassing run() + $this->changedFilesDetector->setActiveScope($configuration->getOnlyRule(), $configuration->getOnlySuffix()); + /** @var SystemError[] $systemErrors */ $systemErrors = []; @@ -179,11 +185,8 @@ private function processFile(File $file, Configuration $configuration): FileProc if ($fileProcessResult->getSystemErrors() !== []) { $this->changedFilesDetector->invalidateFile($file->getFilePath()); } elseif (! $configuration->isDryRun() || ! $fileProcessResult->getFileDiff() instanceof FileDiff) { - // a file clean under a subset of rules is not necessarily clean under all rules, - // caching it would hide its pending changes from the next full run - if ($configuration->getOnlyRule() === null && $configuration->getOnlySuffix() === null) { - $this->changedFilesDetector->cacheFile($file->getFilePath()); - } + // selective runs are safe to cache now — the key is scoped to the rule selection + $this->changedFilesDetector->cacheFile($file->getFilePath()); } return $fileProcessResult; diff --git a/src/Caching/Detector/ChangedFilesDetector.php b/src/Caching/Detector/ChangedFilesDetector.php index 46718bab661..93bc528806f 100644 --- a/src/Caching/Detector/ChangedFilesDetector.php +++ b/src/Caching/Detector/ChangedFilesDetector.php @@ -21,6 +21,9 @@ final class ChangedFilesDetector */ private array $cacheableFiles = []; + // scopes the per-file cache key to the active --only / --only-suffix selection (empty = full run) + private string $scopeSuffix = ''; + public function __construct( private readonly FileHashComputer $fileHashComputer, private readonly Cache $cache, @@ -28,6 +31,14 @@ public function __construct( ) { } + public function setActiveScope(?string $onlyRule, ?string $onlySuffix): void + { + // each selection gets its own cache key, so --only and full runs coexist without clearing or poisoning + $this->scopeSuffix = ($onlyRule === null && $onlySuffix === null) + ? '' + : '|only:' . ($onlyRule ?? '') . '|suffix:' . ($onlySuffix ?? ''); + } + public function cacheFile(string $filePath): void { $filePathCacheKey = $this->getFilePathCacheKey($filePath); @@ -95,7 +106,7 @@ private function resolvePath(string $filePath): string private function getFilePathCacheKey(string $filePath): string { - return $this->fileHasher->hash($this->resolvePath($filePath)); + return $this->fileHasher->hash($this->resolvePath($filePath) . $this->scopeSuffix); } private function hashFile(string $filePath): string diff --git a/tests/Application/ApplicationFileProcessor/ApplicationFileProcessorTest.php b/tests/Application/ApplicationFileProcessor/ApplicationFileProcessorTest.php index 09c75c0c246..286a9b64ef9 100644 --- a/tests/Application/ApplicationFileProcessor/ApplicationFileProcessorTest.php +++ b/tests/Application/ApplicationFileProcessor/ApplicationFileProcessorTest.php @@ -38,7 +38,7 @@ public function testCleanFileIsCachedAsUnchanged(): void $this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath)); } - public function testOnlyRuleRunDoesNotCacheFileAsUnchanged(): void + public function testOnlyRuleRunCachesUnderOwnScopeWithoutPoisoningFullRun(): void { $filePath = __DIR__ . '/Source/CleanFile.php'; @@ -47,11 +47,16 @@ public function testOnlyRuleRunDoesNotCacheFileAsUnchanged(): void onlyRule: RemoveEmptyClassMethodRector::class )); - // a file clean under one rule is not necessarily clean under all rules + // a repeated --only run hits its own scoped cache entry + $this->changedFilesDetector->setActiveScope(RemoveEmptyClassMethodRector::class, null); + $this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath)); + + // a full run uses a different scope key, so it is not poisoned + $this->changedFilesDetector->setActiveScope(null, null); $this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath)); } - public function testOnlySuffixRunDoesNotCacheFileAsUnchanged(): void + public function testOnlySuffixRunCachesUnderOwnScopeWithoutPoisoningFullRun(): void { $filePath = __DIR__ . '/Source/CleanFile.php'; @@ -60,6 +65,10 @@ public function testOnlySuffixRunDoesNotCacheFileAsUnchanged(): void onlySuffix: 'Controller.php' )); + $this->changedFilesDetector->setActiveScope(null, 'Controller.php'); + $this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath)); + + $this->changedFilesDetector->setActiveScope(null, null); $this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath)); } }