Fix --only runs caching files as unchanged, hiding pending changes from full runs#8029
Conversation
A file clean under one rule is not necessarily clean under all rules. An --only run cached such files as unchanged, so the next full run silently skipped their pending changes until --clear-cache. Covered by a new e2e scenario that fails on main: an --only run followed by a full run must still report the change pending under the other rule. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cache only checked each file's own content, so a clean file stayed skipped on warm runs even when one of its dependencies changed, e.g. a parent class method gaining a return type that lets a child file infer its own. A fresh run reports the new change, a warm run misses it. PHPStanNodeScopeResolver now records each file's dependencies during scope resolution using PHPStan's own DependencyResolver. Cache entries store the file's own hash plus one hash per dependency, all re-validated on load; legacy string entries self-upgrade on the next write. A failed capture skips caching entirely rather than caching a partial set. To keep capture cheap, only nodes whose resolver branch can produce dependencies are resolved, and native function calls are skipped via NativeFunctionCallAnalyzer, avoiding PHPStan's function signature map load. Two tripwire tests pin the external assumptions: one parses the bundled DependencyResolver and fails if its branch set changes on a PHPStan bump, the other asserts native function reflections stay fileless. Selective runs (--only, --only-suffix) bypass the cache write, same guard as rectorphp#8029. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks for the fix 👍 e2e seems heavy for such a verfication. |
ApplicationFileProcessor::processFiles() with no rules registered reaches the cache-write branch directly, so the guard is testable without an e2e project: an --only / --only-suffix run must leave the file uncached, a plain dry-run caches it. Fails on main, passes with the fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good call — replaced the e2e with a unit test, PR is now 81 lines total.
|
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cache only checked each file's own content, so a clean file stayed skipped on warm runs even when one of its dependencies changed, e.g. a parent class method gaining a return type that lets a child file infer its own. A fresh run reports the new change, a warm run misses it. PHPStanNodeScopeResolver now records each file's dependencies during scope resolution using PHPStan's own DependencyResolver, the same engine behind PHPStan's result cache. Cache entries store the file's own hash plus one hash per dependency, all re-validated on load; legacy string entries self-upgrade on the next write. A failed capture skips caching entirely rather than caching a partial set. Function calls memoize their dependency files per resolved name, as signature dependencies are identical at every call site. Selective runs (--only, --only-suffix) bypass the cache write, same guard as rectorphp#8029. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cache only checked each file's own content, so a clean file stayed skipped on warm runs even when one of its dependencies changed, e.g. a parent class method gaining a return type that lets a child file infer its own. A fresh run reports the new change, a warm run misses it. PHPStanNodeScopeResolver now records each file's dependencies during scope resolution using PHPStan's own DependencyResolver, the same engine behind PHPStan's result cache. Cache entries store the file's own hash plus one hash per dependency, all re-validated on load; legacy string entries self-upgrade on the next write. A failed capture skips caching entirely rather than caching a partial set. Function calls memoize their dependency files per resolved name, as signature dependencies are identical at every call site. Selective runs (--only, --only-suffix) bypass the cache write, same guard as rectorphp#8029. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
I'd love to merge this one. @samsonasik Can you check if its ready? |
|
@TomasVotruba seems ok, the drawback is with this change, the |
|
@samsonasik Hm, that would be a bit counter productive. But cost of having full cache cleared on single |
|
Yeah, I tried to handle this in the past, but it not works well as expected, as the cache hit will back and forth, see old PR: |
|
In that case, I'll go with this approach, seems more intuitive. |
|
Thank you @SanderMuller 👍 |
Problem
A file that is clean under one rule is not necessarily clean under all rules. A run with
--only SomeRule(or--only-suffix) caches such files as unchanged, so the next full run silently skips their pending changes until--clear-cache.Repro on
main:rector process --dry-run --clear-cache --only "Rector\DeadCode\Rector\ClassMethod\RemoveEmptyClassMethodRector"on a file with an always-true if → clean under that rule, file gets cachedrector process --dry-run→ reports[OK] Rector is done!instead of the pendingRemoveAlwaysTrueIfConditionRectorchangeFix
Skip the unchanged-files cache write on selective runs. One guard in
ApplicationFileProcessor.Test
Unit test on
ApplicationFileProcessor::processFiles(): an--only/--only-suffixrun must leave the file uncached, a plain dry-run caches it (control test). The two guard tests fail onmain, pass with the fix.Split out of #8028 as a standalone pre-existing bugfix, as discussed with @TomasVotruba.
🤖 Generated with Claude Code