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
12 changes: 11 additions & 1 deletion src/Caching/Config/FileHashComputer.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,28 @@
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);

// relative config path, so the hash (and the whole cache) is not tied to one directory
$relativeFilePath = $this->filePathHelper->relativePath($this->filePathHelper->resolveRealPath($filePath));

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

private function ensureIsPhp(string $filePath): void
Expand Down
22 changes: 10 additions & 12 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 @@ -105,24 +107,20 @@ public function setFirstResolvedConfigFileInfo(string $filePath): void
$this->storeConfigurationDataHash($filePath, $configurationSnapshot);
}

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

return $realPath;
return $this->fileHasher->hash($this->cacheKeyPath($filePath) . $this->scopeSuffix);
}

private function getFilePathCacheKey(string $filePath): string
// relative to the project, so a cache built in one checkout is reused in another (worktree, CI, container mount)
private function cacheKeyPath(string $filePath): string
{
return $this->fileHasher->hash($this->resolvePath($filePath) . $this->scopeSuffix);
return $this->filePathHelper->relativePath($this->filePathHelper->resolveRealPath($filePath));
}

private function hashFile(string $filePath): string
{
return $this->fileHasher->hashFiles([$this->resolvePath($filePath)]);
return $this->fileHasher->hashFiles([$this->filePathHelper->resolveRealPath($filePath)]);
}

/**
Expand Down
77 changes: 74 additions & 3 deletions src/Configuration/Parameter/SimpleParameterProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ final class SimpleParameterProvider
Option::SKIP,
];

/**
* Absolute-path options anchored to the project before hashing, so a checkout at a different
* path is not read as a changed configuration.
* @var array<Option::*>
*/
private const array CACHE_PROJECT_PATH_PARAMETER_NAMES = [
Option::PATHS,
Option::AUTOLOAD_PATHS,
Option::BOOTSTRAP_FILES,
];

/**
* @var array<string, mixed>
*/
Expand Down Expand Up @@ -155,6 +166,18 @@ public static function hashForCacheInvalidation(): string
unset($strictParameters[$ignoredName]);
}

$projectPathPrefix = self::projectPathPrefix();
foreach (self::CACHE_PROJECT_PATH_PARAMETER_NAMES as $pathParameterName) {
if (! isset($strictParameters[$pathParameterName]) || ! is_array($strictParameters[$pathParameterName])) {
continue;
}

$strictParameters[$pathParameterName] = self::relativizeProjectPaths(
$strictParameters[$pathParameterName],
$projectPathPrefix
);
}

ksort($strictParameters);

return sha1(serialize($strictParameters));
Expand All @@ -166,13 +189,61 @@ 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
),
];
}

/**
* Strips the project prefix off absolute config paths, so the same configuration hashes alike
* across checkouts instead of tying the cache to one directory.
*
* @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
11 changes: 11 additions & 0 deletions src/FileSystem/FilePathHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,17 @@ public function __construct(
) {
}

// real path when it resolves, the original path otherwise (vfs, phar, not-yet-created)
public function resolveRealPath(string $filePath): string
{
$realPath = realpath($filePath);
if ($realPath === false) {
return $filePath;
}

return $realPath;
}

public function relativePath(string $fileRealPath): string
{
if (! $this->filesystem->isAbsolutePath($fileRealPath)) {
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