Skip to content
Open
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
24 changes: 23 additions & 1 deletion src/Caching/Config/FileHashComputer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,40 @@
use Rector\Application\VersionResolver;
use Rector\Configuration\Parameter\SimpleParameterProvider;
use Rector\Exception\ShouldNotHappenException;
use Rector\FileSystem\FilePathHelper;

/**
* Inspired by https://github.com/symplify/easy-coding-standard/blob/e598ab54686e416788f28fcfe007fd08e0f371d9/packages/changed-files-detector/src/FileHashComputer.php
*/
final readonly class FileHashComputer
{
public function __construct(
private FilePathHelper $filePathHelper
) {
}

public function compute(string $filePath): string
{
$this->ensureIsPhp($filePath);

$parametersHash = SimpleParameterProvider::hashForCacheInvalidation();
return sha1($filePath . $parametersHash . VersionResolver::PACKAGE_VERSION);

// the config path is relative to the project: an absolute one ties the hash, and with it
// the whole cache, to a single directory on a single machine. Resolved first, because the
// path arrives straight from `--config` and two spellings of one file must hash alike.
$relativeFilePath = $this->filePathHelper->relativePath($this->resolvePath($filePath));

return sha1($relativeFilePath . $parametersHash . VersionResolver::PACKAGE_VERSION);
}

private function resolvePath(string $filePath): string
{
$realPath = realpath($filePath);
if ($realPath === false) {
return $filePath;
}

return $realPath;
}

private function ensureIsPhp(string $filePath): void
Expand Down
21 changes: 18 additions & 3 deletions src/Caching/Detector/ChangedFilesDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Rector\Caching\Config\FileHashComputer;
use Rector\Caching\Enum\CacheKey;
use Rector\Configuration\Parameter\SimpleParameterProvider;
use Rector\FileSystem\FilePathHelper;
use Rector\Util\FileHasher;

/**
Expand All @@ -28,7 +29,8 @@ final class ChangedFilesDetector
public function __construct(
private readonly FileHashComputer $fileHashComputer,
private readonly Cache $cache,
private readonly FileHasher $fileHasher
private readonly FileHasher $fileHasher,
private readonly FilePathHelper $filePathHelper
) {
}

Expand Down Expand Up @@ -70,7 +72,7 @@ public function hasFileChanged(string $filePath): bool
// 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));
$unscopedCacheKey = $this->fileHasher->hash($this->cacheKeyPath($filePath));
$cachedValue = $this->cache->load($unscopedCacheKey, CacheKey::FILE_HASH_KEY);
}

Expand Down Expand Up @@ -117,7 +119,20 @@ private function resolvePath(string $filePath): string

private function getFilePathCacheKey(string $filePath): string
{
return $this->fileHasher->hash($this->resolvePath($filePath) . $this->scopeSuffix);
return $this->fileHasher->hash($this->cacheKeyPath($filePath) . $this->scopeSuffix);
}

/**
* The path a cache key is built from: relative to the project, never absolute.
*
* An absolute path ties the whole cache to one location on disk, so the same project
* checked out twice - a git worktree, a CI checkout, a container mount - shares nothing.
* Relative keys let a cache travel with the project. Paths outside the project keep
* their `../` prefix and stay just as stable, because the anchor does not move either.
*/
private function cacheKeyPath(string $filePath): string
{
return $this->filePathHelper->relativePath($this->resolvePath($filePath));
}

private function hashFile(string $filePath): string
Expand Down
60 changes: 56 additions & 4 deletions src/Configuration/Parameter/SimpleParameterProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public static function hashForCacheInvalidation(): string

ksort($strictParameters);

return sha1(serialize($strictParameters));
return sha1(serialize(self::relativizeProjectPaths($strictParameters, self::projectPathPrefix())));
}

/**
Expand All @@ -166,13 +166,65 @@ public static function hashForCacheInvalidation(): string
*/
public static function provideCacheDirectionalParameters(): array
{
$projectPathPrefix = self::projectPathPrefix();

return [
'rules' => self::$parameters[Option::REGISTERED_RECTOR_RULES] ?? [],
'sets' => self::$parameters[Option::REGISTERED_RECTOR_SETS] ?? [],
'skip' => self::$parameters[Option::SKIP] ?? [],
'rules' => self::relativizeProjectPaths(
(array) (self::$parameters[Option::REGISTERED_RECTOR_RULES] ?? []),
$projectPathPrefix
),
'sets' => self::relativizeProjectPaths(
(array) (self::$parameters[Option::REGISTERED_RECTOR_SETS] ?? []),
$projectPathPrefix
),
'skip' => self::relativizeProjectPaths(
(array) (self::$parameters[Option::SKIP] ?? []),
$projectPathPrefix
),
];
}

/**
* Paths declared in the configuration - analysed paths, autoload and bootstrap files, set
* files - are absolute, so they carry the location of the project into the cache identity.
* Hashed as they are, the cache is bound to one directory: a git worktree, a second
* checkout or a CI cache restored under a different workspace name looks like a changed
* configuration and drops every entry on its first run. Anchored to the project instead,
* they describe the same configuration wherever it is checked out.
*
* @param mixed[] $parameters
* @return mixed[]
*/
private static function relativizeProjectPaths(array $parameters, string $projectPathPrefix): array
{
foreach ($parameters as $key => $value) {
if (is_array($value)) {
$parameters[$key] = self::relativizeProjectPaths($value, $projectPathPrefix);
continue;
}

if (is_string($value) && str_starts_with($value, $projectPathPrefix)) {
$parameters[$key] = substr($value, strlen($projectPathPrefix));
}
}

return $parameters;
}

/**
* Empty when the working directory cannot be resolved, which makes the relativizing above a
* no-op rather than a wrong answer.
*/
private static function projectPathPrefix(): string
{
$currentDirectory = getcwd();
if ($currentDirectory === false) {
return '';
}

return rtrim($currentDirectory, '/') . '/';
}

/**
* @param Option::* $name
*/
Expand Down
4 changes: 3 additions & 1 deletion tests/Bootstrap/AutoloadFileParameterResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
use Rector\Caching\Config\FileHashComputer;
use Rector\Configuration\Option;
use Rector\Configuration\Parameter\SimpleParameterProvider;
use Rector\FileSystem\FilePathHelper;
use Symfony\Component\Filesystem\Filesystem;

final class AutoloadFileParameterResolverTest extends TestCase
{
Expand Down Expand Up @@ -58,7 +60,7 @@ public function testWithoutOptionParameterStaysUntouched(): void

public function testResolvedAutoloadFileChangesConfigurationHash(): void
{
$fileHashComputer = new FileHashComputer();
$fileHashComputer = new FileHashComputer(new FilePathHelper(new Filesystem()));
$configFilePath = __DIR__ . '/config/some_config.php';

$hashWithout = $fileHashComputer->compute($configFilePath);
Expand Down
Loading
Loading