| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
I don't think this has to be behind bleeding edge. It's not a BC break, it should work the same way as before. It's a good point that some people might be doing something to the structure of the current result cache format to achieve similar things this PR aims to achieve, but that's out of BC surface anyway, and the end result should be that they should have to be able delete their tooling altogether, if we do a good job. As a bonus, more E2E result cache tests will test the new path. |
Sorry, something went wrong.
|
Done, made it the default and dropped the featureToggle. Since the on-disk format changes I bumped CACHE_VERSION, so the first run after upgrading is a cold one and everything is relative from then on. And yes, every result cache e2e now exercises the relative path, on top of the dedicated fixture and the git worktree test. |
Sorry, something went wrong.
| cp -al ../../vendor "$WORKTREE/vendor" | ||
| cp -R tmp "$WORKTREE/e2e/result-cache-relative-path/tmp" | ||
| rm -rf "$WORKTREE/e2e/result-cache-relative-path/tmp/cache" |
There was a problem hiding this comment.
did I understand it right, that we need this copying because this PR is not going to implement the whole feature in a single shot?
Sorry, something went wrong.
There was a problem hiding this comment.
Yes, exactly. The copy stands in for cache discovery, PHPStan locating the main checkout's warm cache from a fresh worktree, which is the harder part and a deliberate follow-up, not this PR. This PR makes the cache content portable so it can be reused once it is present; getting it present in a bare worktree is the separate step. I noted it in the code comment and the PR description.
Sorry, something went wrong.
There was a problem hiding this comment.
I think it looks good. lets see what @ondrejmirtes thinks about it
Sorry, something went wrong.
|
(just see CI is failling now) |
Sorry, something went wrong.
Looking into it. Yesterday GitHub had an outage so couldn't properly follow-up on the CI |
Sorry, something went wrong.
|
Would be nice if you ran it locally and grepped the new result cache whether there are still some absolute paths present or not. Also the cache should be portable between Windows and Linux so the directory separators should always be /. PHPStan has FileHelper class for normalization. |
Sorry, something went wrong.
|
Did both. I ran self-analysis and grepped the generated result cache for the absolute project prefix: 0 occurrences. Analysed files, the dependency graph, and the meta (including composerInstalled install_path and the stub files) are all stored relative to the anchor. Paths with no shared prefix with the anchor, e.g. a tmpDir pointed outside the project tree, stay absolute by design, matching ccache's CCACHE_BASEDIR rule; in a normal project tmpDir is under the project and gets relativized too. On separators, stored paths now always use /. The relative path helper already emits / for anything reachable from the anchor, so this only covered the no-shared-prefix edge, which on Windows would otherwise keep backslashes. On load the paths are absolutized back to the OS-native separator that FileFinder uses for the analysed-file keys, so a cache written on Windows re-absolutizes on Linux and vice versa. Namespace separators in class names are left as-is; only path values are normalised. |
Sorry, something went wrong.
|
just noticed, that this PR has another nice side effect: result-cache size shrinks by ~5% in comparison to 2.2.x on my machine running on phpstan-src ➜ phpstan-src git:(pr/6190) ls -la tmp/resultCache.php -rw-r--r--@ 1 staabm staff 40814358 Aug 7 13:32 tmp/resultCache.php ➜ phpstan-src git:(2.2.x) ls -la tmp/resultCache.php -rw-r--r--@ 1 staabm staff 42653306 Aug 7 13:33 tmp/resultCache.php `` |
Sorry, something went wrong.
|
Nice, that makes sense: every stored path drops the absolute prefix (on your checkout the /Users/staabm/.../phpstan-src/ part), and each path shows up many times in the cache, as the section keys, in the dependency lists, and in the error and linesToIgnore entries, so it adds up quickly. It also scales with how deep the checkout lives: a CI path like /home/runner/work/<repo>/<repo>/ is longer than a local one, so the relative form saves even more there. |
Sorry, something went wrong.
|
Sorry, something went wrong.
|
Description updated. On CI: the failures were a real one from me. Removing the unit test earlier orphaned a ResultCachePathTransformer method, self-analysis flagged it as unused, and that one error failed every make phpstan-based job (plain, with result cache, generate-baseline, mutation, turbo). Fixed in 9e1943f by dropping the unused method, and those jobs are green again. The only red left is Other Tests (cd e2e/bug8778), which fails at the turbo-extension artifact download with digest-mismatch: error before the test runs. That is infra, not from this PR (the same artifact digest-mismatch shows up on other PRs too). |
Sorry, something went wrong.
|
Dogfooding this on real projects turned up a blocker: the phar never reuses the result cache. This is not a portability problem, it misses in place, with nothing moved. Minimal repro, a single clean file at level 5, no vendor and no extensions, second identical run: # phar from this PR Result cache not used because the metadata do not match: executedFilesHashes # phar from 2.2.x Result cache restored. 0 files will be reanalysed. Two real projects behave the same way with --fail-without-result-cache on an identical rerun: both write a cache and then miss it, where the 2.2.x phar restores it. The cache file is consistently a bit smaller than the 2.2.x one, so relativization is working, it just is not reversible. Root cause. getExecutedFileHashes() hashes cliAutoloadFile plus bootstrapFiles, and conf/config.neon ships four bootstrap files that live inside the phar (stubs/runtime/ReflectionUnionType.php, ReflectionAttribute.php, Attribute85.php, ReflectionIntersectionType.php). The stored keys differ like this: 2.2.x: phar:///abs/path/phpstan.phar/stubs/runtime/Attribute85.php this PR: phpstan.phar/stubs/runtime/Attribute85.php The transformer treats the phar:// URL as an ordinary filesystem path, so the scheme is dropped and what remains is relative to the directory holding the phar. Since the scheme is gone from the stored string, absolutizing on load cannot put it back, so the restored key can never equal the phar://... key the current run computes. executedFilesHashes therefore differs on every run and the cache is thrown away. Why the tests here do not catch it. Run from bin/phpstan at the same commit, the cache is reused normally (Result cache restored. 0 files will be reanalysed.), because the bootstrap files are then plain filesystem paths. Only the phar is affected, which is also why CI was already pointing at it: the Compile PHAR run for this branch fails e2e/bug8778 with exactly metadata do not match: executedFilesHashes, and those two jobs pass on the other PRs I compared against. Fix direction. Make the path transformer phar://-aware and leave phar-internal paths absolute. They belong to the tool rather than to the project, so their location has no bearing on cache portability, and rewriting them can only break the round trip. I will look at the fix. Flagging it now because @staabm asked people on phpstan/phpstan#8599 to try this artifact, and as built it will look like the feature does nothing. |
Sorry, something went wrong.
|
Fixed in 6dddb50. The phar reuses its result cache again, and I have to correct the fix direction I gave earlier. I said the transformer should leave phar-internal paths absolute, because they belong to the tool and rewriting them "can only break the round trip". The first half is fine, the reasoning is not. Dropping the scheme is what breaks the round trip, not rewriting the path. So the fix splits the scheme off, relativizes only the filesystem path behind it, and restores the scheme verbatim: before: phar:///abs/project/vendor/bin/phpstan.phar/stubs/runtime/Attribute85.php stored: phar://phpstan.phar/stubs/runtime/Attribute85.php restored: phar:///abs/project/vendor/bin/phpstan.phar/stubs/runtime/Attribute85.php Leaving them absolute would have been the smaller change, and it does restore in-place reuse, but I measured it and it forfeits the actual goal: with the phar sitting in the project's vendor, its absolute path moves with the project, so executedFilesHashes still differed after a move and the cache was still discarded. Preserving the scheme gets both. Built a phar from the branch and checked all three cases:
Two other things worth noting. Error no longer reimplements the rewriting. It had relativizePaths(RelativePathHelper) and absolutizePaths(FileHelper), which duplicated what the transformer does and would have needed the same scheme handling bolted on separately. It now has one transformPaths(callable) that the transformer calls with its own relativizePath()/absolutizePath(), so an error's paths and the cache's file-path keys cannot drift apart again. Both methods are new in this PR, so nothing released changes. The gap was that nothing exercised a phar:// path outside the compiled-phar job. Added e2e/result-cache-phar-bootstrap, which builds a small phar, registers a bootstrap file inside it and asserts that no scheme-less key is stored and that an identical rerun reuses the cache. It runs against bin/phpstan, and both assertions fail without the fix, so this cannot regress silently in the source build any more. Gates: full suite green (21300 tests), self-analysis clean, phpcs clean, and the new e2e verified in both directions. e2e/bug8778 in the Compile PHAR job should go green now too, since that was the same executedFilesHashes mismatch. @staabm the artifact is worth retesting now - as built before this commit it genuinely did nothing for a phar install. |
Sorry, something went wrong.
|
Retested on the two real projects that originally showed the miss, with a phar built from 6dddb503b. Both reuse the cache again, matching base:
Second identical run each time, --fail-without-result-cache, cold cache before run 1. Both projects analyse clean, so exit 1 cannot mask exit 2 here. That only shows the regression is gone, though. To check the feature itself I installed the phar the way Composer does, at vendor/bin/phpstan.phar inside the project, warmed the cache, then copied the whole project (a real cp -R, not a symlink) to a different absolute path:
So on a real project a phar install now survives the move, which is the thing the PR is for. My earlier evidence for the moved case was a synthetic single-file project, and the phar-installed case was exactly where it was broken, so I did not want to leave that on inference. One check worth mentioning because the version string is misleading: the phar I built reports 2.2.x-dev@bba3c00, which is not the branch head, so I verified the contents instead of the label — read src/Analyser/ResultCache/ResultCachePathTransformer.php out of the phar and confirmed splitScheme() is present (and Error::transformPaths()), both absent from the pre-fix phar. |
Sorry, something went wrong.
Sorry, something went wrong.
|
The same problem is visible even when analysing phpstan-src here. It's easy to grep for absolute path of the project's checkout. |
Sorry, something went wrong.
|
Fixed in d229daf, along with a second bug the same code path was hiding. Collected dataImplemented your suggestion: a CollectorWithPaths interface extending Collector, which the cache calls when saving and loading. Two things I decided while writing it, both easy to change if you disagree:
On the Error|array<mixed> union you mentioned: the array form is what an Error becomes after crossing a parallel worker boundary, so the helper handles both — Error::decode() then transformPaths() then jsonSerialize(), reusing the existing round trip rather than hand-picking keys. Analysing phpstan-src now leaves 0 occurrences of the checkout path in resultCache.php, down from 284. The second bug: mangled class names in the compound file fieldGrepping for the checkout path, as you suggested, also turned up something worse in code I had already written. Error::getFile() returns the compound path (in context of class X) string, and I was relativizing it as if the whole thing were a path — so the forward-slash normalisation that makes the cache portable between Windows and Linux was rewriting the backslashes in the class name too: 'file' => 'src/Type/JustNullableTypeTrait.php (in context of class PHPStan/Type/BooleanType)' That is on every error reported in a trait, not just collected data, and it survives the round trip — so a warm run served a mangled class name in that field. The transformer already had compound-aware helpers (relativizeCompoundKey/absolutizeCompoundKey, used for linesToIgnore keys); Error's paths now go through them, and the class name stays PHPStan\Type\BooleanType. Teste2e/result-cache-relative-path now has a trait with a constant condition, so the in-trait collector actually fires there, and the assertion changed from "the analysed file is not stored under its absolute path" to "the checkout prefix appears nowhere in the cache file", printing the offenders when it does. Verified it fails without the fix (6 absolute paths) and passes with it. Gates: full suite green (21300), self-analysis clean, phpcs clean, and the warm run still restores with 0 files reanalysed. One thing I did not do: nothing checks that a new collector carrying paths remembers to implement the interface. A rule could require it when a collector's value type mentions Error, but that seemed like scope creep here — say the word if you want it. |
Sorry, something went wrong.
…ggle The result cache stores absolute paths in its meta, keys and stored objects, and compares metadata with a strict whole-array match, so a changed absolute prefix (a fresh CI checkout dir, a git worktree) throws the whole cache away even when the relative layout is identical. Add a bleeding-edge featureToggle, relativePathResultCache, that stores the paths relative to the phpstan install (%rootDir%) and re-absolutizes them against the current install on load. Only paths reachable from the anchor become relative; the rest stay absolute, following ccache's rule. Error gains relativizePaths()/absolutizePaths(), building on its existing immutable changeFilePath() pattern, and a new ResultCachePathTransformer handles the rest of the cache structure at the save/restore boundary. The toggle state is folded into the cache meta and CACHE_VERSION is bumped so flipping it or upgrading migrates with one cold run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Warms the cache in one checkout, creates a git worktree at a different absolute path with its own phpstan install, carries the warm cache over, and asserts it is reused with 0 files reanalysed. Proves the relative paths re-absolutize against the worktree, the scenario the toggle targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PHPStan's -vv progress, including the "Result cache restored" line, is written to stderr. The assertion captured stdout only, so it missed the message and failed even though the cache was reused. Redirect stderr into the captured output. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: this is not a BC break. For a project analysed on the same machine the relativized paths re-absolutize to the exact same absolute paths, so behaviour is unchanged; the only difference is that a moved project (a CI checkout dir, a git worktree) now reuses the cache instead of discarding it. Drop the featureToggle and relativize/absolutize unconditionally. The CACHE_VERSION bump migrates old caches with one cold run, and every result cache e2e now exercises the new path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the redundant "relative path is present" assertion (the explicit "absolute path is absent" check on the next line already proves the path was relativized), fix a stale "toggle on" comment, and after the git worktree run remove the worktree and re-run in the original checkout to confirm it still reuses its own cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rage The transformer's behaviour is covered end to end by the result cache e2e tests (relative storage, warm reuse, and reuse across a git worktree at a different absolute path). The unit test additionally hardcoded POSIX absolute paths, which the Windows Tests matrix cannot satisfy since path handling there is drive-letter based. Drop it, matching the project's convention of testing result cache behaviour through e2e fixtures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The relative path helper already emits '/'-separated paths for anything reachable from the anchor, but returns a path with no shared prefix unchanged, which on Windows keeps backslashes. Normalise the stored paths to '/' so a cache written on Windows is usable on Linux and vice versa. On load the paths are absolutized back to the OS-native separator that FileFinder uses for analysed-file keys. Namespace separators in class names (FQCNs) are untouched, only path values are normalised. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
projectConfig is stored as a relative Neon string and is never absolutized on load; the metadata comparison relativizes the current config instead. So absolutizeProjectConfig() had no caller once the unit test was removed and self-analysis flagged it. Inline the remaining relativize-only logic into relativizeProjectConfig() and drop the now single-use helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A bootstrapFile that lives inside a phar reaches the path transformer as a phar:// URL, and PHPStan registers the four runtime stubs it ships inside phpstan.phar exactly that way. getRelativePath() treated the URL as a plain filesystem path and dropped the scheme, which absolutizePath() cannot put back: the restored key could never equal the phar://... key the next run computes, so executedFilesHashes differed on every run and the cache was thrown away. A phar install never reused its result cache at all. Split the scheme off, rewrite only the filesystem path behind it, and restore the scheme verbatim. That keeps the round trip lossless and keeps the phar's own path portable, so a phar install now also survives the project moving to a different absolute path. Error::relativizePaths()/absolutizePaths() are replaced by a single transformPaths(callable) so the error's paths go through the very same relativizePath()/absolutizePath() as the cache's file-path keys, instead of reimplementing them against a RelativePathHelper and a FileHelper. Both directions are covered by the new result-cache-phar-bootstrap e2e, which builds a small phar, registers a bootstrap file inside it, and asserts that no scheme-less key is stored and that an identical rerun reuses the cache.
Collected data is opaque to the result cache - only the collector that produced it knows where paths sit inside its value - so the cache rewrote the file-path keys and left every path inside the values absolute. The constant-condition collectors carry the reported Error in their collected value, so the cache still embedded the absolute checkout path: 284 occurrences when analysing phpstan-src itself. Collectors that carry paths now say so by implementing CollectorWithPaths, which the cache calls when it saves and loads. The method is static because the cache only ever holds the collector's class name, never an instance. Also fixes the compound "path (in context of class X)" form of Error::getFile(): it was relativized as if the whole string were a path, so the forward-slash normalisation that makes the cache portable between Windows and Linux rewrote the backslashes in the class name too, and an error reported in a trait came back from the cache with a mangled class name in that field. The transformer already had compound-aware helpers for linesToIgnore's keys; Error's paths now go through them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thank you! |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Makes the result cache portable across a change of the project's absolute path prefix (a fresh CI checkout dir, a git worktree). Today the cache stores absolute paths everywhere (meta, storage keys, stored objects) and compares metadata with a strict whole-array match, so a moved project with an identical relative layout throws the whole cache away.
Approach
Covered surface
Storage: errors, locallyIgnoredErrors, linesToIgnore/unmatchedLineIgnores (including the compound path (in context of class X) keys), collectedData, dependencies, packageDependencies, exportedNodes, projectExtensionFiles. Meta: analysedPaths, scannedFiles, composerLocks, composerInstalled (including nested install_path), executedFilesHashes, stubFiles, and projectConfig paths/tmpDir.
Verified by grepping a real self-analysis cache: 0 absolute project-prefix paths remain.
Tests
Open question: the anchor
%rootDir% is portable when the phar moves with the project (Composer install, worktree, CI checkout). A globally-installed phpstan.phar outside the project tree does not share a moving prefix, so its relative offsets would not survive a move. %currentWorkingDirectory% moves with the project in those same scenarios and matches the issue's wording, at the cost of not covering PHPStan's own stub and config paths. The anchor is isolated behind getPathTransformer() plus the injected %rootDir%, so switching is a one-line change if preferred.
Follow-ups (not in this PR)
Closes phpstan/phpstan#8599