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 👍 |
rectorphp#8029 stopped caching selective runs to fix full-run poisoning (a file clean under one rule was cached as clean for all). That also removed the cache on repeated --only / --only-suffix runs — a drawback raised in that thread, and what rectorphp#7641 set out to solve. Scope the per-file cache key by the active rule selection instead. Selective and full runs then use distinct, coexisting cache entries: a repeated --only run is served from cache, a full run is never poisoned by it, and nothing is cleared, so there is no back-and-forth thrash (the behaviour that closed rectorphp#7641). The scope is set in both run() and processFiles() because parallel workers invoke processFiles() directly via WorkerCommand, bypassing run(), and must write entries under the same scope the main process reads.
Thanks for the pointer, can you see if #8075 got this working? |
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