-
-
Notifications
You must be signed in to change notification settings - Fork 439
Fix unchanged-files cache missing changes induced by changed dependencies #8028
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,154 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Rector\Caching; | ||
|
|
||
| use Rector\Contract\DependencyInjection\ResettableInterface; | ||
| use Rector\Util\FileHasher; | ||
|
|
||
| /** | ||
| * Collects the files each processed file depends on, as surfaced by PHPStan's | ||
| * DependencyResolver during scope resolution, and provides memoized content hashes | ||
| * for dependency validation by the unchanged-files cache. | ||
| */ | ||
| final class FileDependencyCollector implements ResettableInterface | ||
| { | ||
| /** | ||
| * @var array<string, array<string, true>> | ||
| */ | ||
| private array $dependenciesByFile = []; | ||
|
|
||
| /** | ||
| * files whose capture threw, their possibly partial set must never be cached | ||
| * | ||
| * @var array<string, true> | ||
| */ | ||
| private array $failedFiles = []; | ||
|
|
||
| /** | ||
| * keyed by the given path, so memo hits skip realpath() as well | ||
| * | ||
| * @var array<string, string|null> | ||
| */ | ||
| private array $contentHashMemo = []; | ||
|
|
||
| /** | ||
| * a function's signature dependencies are identical at every call site | ||
| * | ||
| * @var array<string, string[]> | ||
| */ | ||
| private array $functionDependencyFilesMemo = []; | ||
|
|
||
| public function __construct( | ||
| private readonly FileHasher $fileHasher, | ||
| ) { | ||
| } | ||
|
|
||
| public function record(string $filePath, string $dependencyFilePath): void | ||
| { | ||
| if ($filePath === $dependencyFilePath) { | ||
| return; | ||
| } | ||
|
|
||
| $this->dependenciesByFile[$filePath][$dependencyFilePath] = true; | ||
| } | ||
|
|
||
| public function markFailed(string $filePath): void | ||
| { | ||
| $this->failedFiles[$filePath] = true; | ||
| } | ||
|
|
||
| /** | ||
| * @return string[]|null | ||
| */ | ||
| public function getMemoizedFunctionDependencyFiles(string $functionKey): ?array | ||
| { | ||
| return $this->functionDependencyFilesMemo[$functionKey] ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * @param string[] $dependencyFiles | ||
| */ | ||
| public function memoizeFunctionDependencyFiles(string $functionKey, array $dependencyFiles): void | ||
| { | ||
| $this->functionDependencyFilesMemo[$functionKey] = $dependencyFiles; | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, string>|null null when capture failed and the set cannot be trusted | ||
| */ | ||
| public function getDependencyFileHashes(string $filePath): ?array | ||
| { | ||
| if (isset($this->failedFiles[$filePath])) { | ||
| return null; | ||
| } | ||
|
|
||
| $dependencyHashes = []; | ||
| foreach (array_keys($this->dependenciesByFile[$filePath] ?? []) as $dependencyFile) { | ||
| $dependencyHash = $this->contentHash($dependencyFile); | ||
| if ($dependencyHash !== null) { | ||
| $dependencyHashes[$dependencyFile] = $dependencyHash; | ||
| } | ||
| } | ||
|
|
||
| return $dependencyHashes; | ||
| } | ||
|
|
||
| /** | ||
| * @param array<string, string> $recordedDependencyHashes | ||
| */ | ||
| public function hasAnyChangedDependency(array $recordedDependencyHashes): bool | ||
| { | ||
| foreach ($recordedDependencyHashes as $dependencyFile => $recordedHash) { | ||
| if ($this->contentHash($dependencyFile) !== $recordedHash) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * null when the file does not exist, e.g. a deleted dependency, which callers treat as changed | ||
| */ | ||
| public function contentHash(string $filePath): ?string | ||
| { | ||
| if (array_key_exists($filePath, $this->contentHashMemo)) { | ||
| return $this->contentHashMemo[$filePath]; | ||
| } | ||
|
|
||
| $resolvedPath = $this->resolvePath($filePath); | ||
| if (! is_file($resolvedPath)) { | ||
| return $this->contentHashMemo[$filePath] = null; | ||
| } | ||
|
|
||
| return $this->contentHashMemo[$filePath] = $this->fileHasher->hashFiles([$resolvedPath]); | ||
| } | ||
|
|
||
| /** | ||
| * drop a memoized hash, e.g. after the file has been written mid-run | ||
| */ | ||
| public function forgetContentHash(string $filePath): void | ||
| { | ||
| unset($this->contentHashMemo[$filePath]); | ||
| } | ||
|
|
||
| public function reset(): void | ||
| { | ||
| $this->dependenciesByFile = []; | ||
| $this->failedFiles = []; | ||
| $this->contentHashMemo = []; | ||
| $this->functionDependencyFilesMemo = []; | ||
| } | ||
|
|
||
| private function resolvePath(string $filePath): string | ||
| { | ||
| $realPath = realpath($filePath); | ||
| if ($realPath === false) { | ||
| return $filePath; | ||
| } | ||
|
|
||
| return $realPath; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace Rector\NodeAnalyzer; | ||
|
|
||
| use PhpParser\Node\Name; | ||
|
|
||
| /** | ||
| * Whether a called function name can only resolve to a native PHP function. | ||
| * Qualified and relative names resolve strictly inside a namespace, use-function | ||
| * imports are honored via the resolvedName attribute. Known limitation: an | ||
| * unimported userland function shadowing a native name in its own namespace is | ||
| * treated as native anyway. Unlike FuncCallNameResolver this stays off | ||
| * ReflectionProvider, as it runs per node during scope resolution. | ||
| * | ||
| * @see \Rector\NodeNameResolver\NodeNameResolver\FuncCallNameResolver | ||
| * @see \Rector\Tests\NodeAnalyzer\NativeFunctionCallAnalyzerTest | ||
| */ | ||
| final class NativeFunctionCallAnalyzer | ||
| { | ||
| /** | ||
| * @var array<string, int>|null | ||
| */ | ||
| private array|null $internalFunctionNames = null; | ||
|
|
||
| public function isNativeFunctionCall(Name $name): bool | ||
| { | ||
| if ($name->isQualified() || $name->isRelative()) { | ||
| return false; | ||
| } | ||
|
|
||
| $this->internalFunctionNames ??= array_flip(get_defined_functions()['internal']); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @samsonasik Do you mean this line then? Would static reflection be faster?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried this AI generated If function check exists needed, there is already phpstan reflection function NativeFunctionReflection for that, so way more reliable per static analysis process instead of runtime php.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The table is built once per process and memoized, not per call - measured in rector's container: ~0.05ms to build, 10k checks ≈ 1.4ms. Calling The namespaced-name comparison answers a different question: "does this resolve to the global namespace", while this guard needs "can this call have a file dependency". Userland functions in the global namespace -
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That means the laravel <?php
use Rector\Config\RectorConfig;
return RectorConfig::configure()
->withBootstrapFiles([__DIR__ . '/bootstrap/app.php'];The reason to use exactly PHPStan reflection as possible is to avoid runtime which inverse with what static analysis purpose. |
||
|
|
||
| $resolvedName = $name->getAttribute('resolvedName'); | ||
| if ($resolvedName instanceof Name) { | ||
| return isset($this->internalFunctionNames[$resolvedName->toLowerString()]); | ||
| } | ||
|
|
||
| return isset($this->internalFunctionNames[$name->toLowerString()]); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.