Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/Caching/Detector/ChangedFilesDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,14 @@ public function addCacheableFile(string $filePath): void

public function hasFileChanged(string $filePath): bool
{
$fileInfoCacheKey = $this->getFilePathCacheKey($filePath);
$cachedValue = $this->cache->load($fileInfoCacheKey, CacheKey::FILE_HASH_KEY);
$cachedValue = $this->cache->load($this->getFilePathCacheKey($filePath), CacheKey::FILE_HASH_KEY);

// a scoped (--only) run reuses the full-run cache: a file left clean by all rules stays
// clean under a single rule too, and the content is still compared below
if ($cachedValue === null && $this->scopeSuffix !== '') {
$unscopedCacheKey = $this->fileHasher->hash($this->resolvePath($filePath));
$cachedValue = $this->cache->load($unscopedCacheKey, CacheKey::FILE_HASH_KEY);
}

if ($cachedValue !== null) {
$currentFileHash = $this->hashFile($filePath);
Expand Down
3 changes: 3 additions & 0 deletions src/Config/RectorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ public function ruleWithConfiguration(string $rectorClass, array $configuration)
$configuration
);

// feed values into the cache hash, so a changed configuration invalidates the cache
SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, $this->ruleConfigurations);

$this->rule($rectorClass);

$this->afterResolving($rectorClass, function (ConfigurableRectorInterface $configurableRector) use (
Expand Down
5 changes: 5 additions & 0 deletions src/Configuration/Option.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,11 @@ final class Option
*/
public const string REGISTERED_RECTOR_SETS = 'registered_rector_sets';

/**
* @internal For cache invalidation when a configurable rule value changes
*/
public const string RULE_CONFIGURATIONS = 'rule_configurations';

/**
* @internal For verify RectorConfigBuilder instance recreated
*/
Expand Down
25 changes: 25 additions & 0 deletions tests/Caching/Config/FileHashComputer/FileHashComputerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ final class FileHashComputerTest extends AbstractLazyTestCase
*/
private array $originalFileExtensions = [];

/**
* @var mixed[]
*/
private array $originalRuleConfigurations = [];

protected function setUp(): void
{
parent::setUp();
Expand All @@ -32,12 +37,14 @@ protected function setUp(): void
// the parameter bag is a global static shared across the whole test process, restore it after
$this->originalRules = SimpleParameterProvider::provideArrayParameter(Option::REGISTERED_RECTOR_RULES);
$this->originalFileExtensions = SimpleParameterProvider::provideArrayParameter(Option::FILE_EXTENSIONS);
$this->originalRuleConfigurations = SimpleParameterProvider::provideArrayParameter(Option::RULE_CONFIGURATIONS);
}

protected function tearDown(): void
{
SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, $this->originalRules);
SimpleParameterProvider::setParameter(Option::FILE_EXTENSIONS, $this->originalFileExtensions);
SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, $this->originalRuleConfigurations);
}

public function testOutputAffectingParameterChangesHash(): void
Expand Down Expand Up @@ -66,4 +73,22 @@ public function testRuleChangeIsExcludedFromHash(): void

$this->assertSame($hashBefore, $hashAfter);
}

public function testConfiguredRuleValueChangeChangesHash(): void
{
$configFilePath = __DIR__ . '/Fixture/rector.php';

SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, [
'Rector\\SomeRule' => ['old value'],
]);
$hashBefore = $this->fileHashComputer->compute($configFilePath);

// same rule, changed configuration value must invalidate the cache
SimpleParameterProvider::setParameter(Option::RULE_CONFIGURATIONS, [
'Rector\\SomeRule' => ['new value'],
]);
$hashAfter = $this->fileHashComputer->compute($configFilePath);

$this->assertNotSame($hashBefore, $hashAfter);
}
}
30 changes: 30 additions & 0 deletions tests/Caching/Detector/ChangedFilesDetectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,36 @@ public function testHasFileChanged(): void
$this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath));
}

public function testScopedRunReusesFullRunCache(): void
{
$filePath = __DIR__ . '/Source/file.php';

// full run caches the file as clean
$this->changedFilesDetector->setActiveScope(null, null);
$this->changedFilesDetector->addCacheableFile($filePath);
$this->changedFilesDetector->cacheFile($filePath);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

// an --only run reuses the full-run cache instead of re-analysing the file
$this->changedFilesDetector->setActiveScope('Rector\\SomeRule', null);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));
}

public function testScopedCacheDoesNotLeakToFullRun(): void
{
$filePath = __DIR__ . '/Source/file.php';

// a scoped run only caches under its own key
$this->changedFilesDetector->setActiveScope('Rector\\SomeRule', null);
$this->changedFilesDetector->addCacheableFile($filePath);
$this->changedFilesDetector->cacheFile($filePath);
$this->assertFalse($this->changedFilesDetector->hasFileChanged($filePath));

// a full run must not treat the file as cached, as only one rule ran
$this->changedFilesDetector->setActiveScope(null, null);
$this->assertTrue($this->changedFilesDetector->hasFileChanged($filePath));
}

public function testCacheKeptWhenRuleRemoved(): void
{
$filePath = $this->cacheFileUnderRules(['Rector\\RuleA', 'Rector\\RuleB']);
Expand Down
Loading