Skip to content

Normalize %placeholder%-expanded path parameters and canonicalize the result cache meta - #6295

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-piyryrg
Open

Normalize %placeholder%-expanded path parameters and canonicalize the result cache meta#6295
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-piyryrg

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

A scanFiles entry written as %rootDir%/../../../vendor/autoload.php discarded the result cache on
every run (Result cache not used because the metadata do not match: scannedFiles, composerInstalled),
while the equivalent placeholder-free vendor/autoload.php entry worked fine.

NeonAdapter absolutizes and normalizes the path entries listed in the expandRelativePaths section,
but skips any value containing a % - those are still unexpanded strings at load time. Such a path
therefore reaches the container with its .. segments intact, while the result cache stores its metadata
relativized and restores it absolutized, which collapses those segments. Restored never equalled
recomputed, so the cache was thrown away every run.

The fix normalizes the path parameters right after Nette expands the placeholders, and additionally
canonicalizes the freshly computed result cache meta with the very same transformation the stored meta
goes through - which also fixes a second, unreported instance of the same round trip being lossy.

Changes

  • src/DependencyInjection/NormalizePathParametersExtension.php (new): a #[ContainerExtension]
    that walks the parameters named by the expandRelativePaths section and runs
    FileHelper::normalizePath() over them. loadConfiguration() is the first point where the
    placeholders are already expanded. It handles the [] "every list element" segments of those config
    keys ([parameters][ignoreErrors][][paths][]), unwraps and rewraps OptionalPath, and deliberately
    leaves still-relative values alone - an fnmatch pattern or a placeholder that expanded to a relative
    value has no config file left to resolve against, and normalizePath() would silently drop its
    leading .. segments.
  • src/Analyser/ResultCache/ResultCacheManager.php: getMeta() now returns
    $this->getPathTransformer()->absolutizeMeta(...), so the computed meta is in the same canonical form
    as the restored one. CACHE_VERSION bumped to v15-canonicalPaths. The ad-hoc configStubFiles
    normalization is dropped - it was a one-off instance of what the canonicalization now does for every
    path-bearing meta key.
  • src/Command/CommandHelper.php: --autoload-file and --configuration are now normalized as
    well as absolutized, matching --generate-baseline, --tmp-file, --instead-of and the analysed
    paths, which already did both.
  • Analogous cases probed and found already correct, so left untouched: FileExcluder,
    IgnoredErrorHelper, FileFinder, DefaultStubFilesProvider and NodeDependencies each normalize
    defensively before comparing (the new extension makes those defences redundant rather than
    load-bearing); allConfigFiles is normalized in ContainerFactory::detectDuplicateIncludedFiles();
    and ReflectionClass::getFileName() is resolved by PHP itself, so Composer's classmap entries
    (__DIR__ . '/../..' . '/src/Foo.php') do not leak .. into projectExtensionFiles.
  • resultCachePath (%tmpDir%/resultCache.php) is not in expandRelativePaths and stays untouched:
    adding it there would also change how a relative resultCachePath is resolved, which is a
    behaviour change unrelated to this bug. It is only ever opened, never compared.

Root cause

The pattern is "the same file spelled two ways": one code path produces a lexically non-canonical
path (a/b/../c) and another produces the canonical one (a/c), and the two are then compared as
strings. Two places were affected:

  1. Config paths containing a %placeholder%. NeonAdapter::process() guards its
    normalizePath(absolutizePath($val)) call with !str_contains($val, '%'), because the placeholder
    is only expanded later by the DI compiler. Every expandRelativePaths key was affected -
    scanFiles, bootstrapFiles, stubFiles, paths, excludePaths, ignoreErrors paths, tmpDir,
    and the rest - not just the scanFiles entry from the report.

  2. Paths PHPStan does not author. ResultCachePathTransformer::absolutizePath() normalizes
    (it must - relativizePath() emits ../.. prefixes that have to collapse against the anchor), but
    the values PHPStan recomputes each run are whatever the source hands it. Composer's
    vendor/composer/installed.php records install_path as __DIR__ . '/../foo/bar', so
    composerInstalled differed on every run in every project with at least one installed package.
    That difference was silently absorbed by the changed-packages fallback, which is why it never showed
    up as a discarded cache - it only printed "Composer metadata changed but no package versions changed"
    on each run and re-resolved the package set for nothing.

Test

  • tests/PHPStan/Command/CommandHelperTest.php gets a dataParameters() case backed by the new
    tests/PHPStan/Command/relative-paths/placeholder-dots.neon, which spells bootstrapFiles,
    scanFiles, scanDirectories, paths, both excludePaths keys and both ignoreErrors path forms
    through %rootDir%/.../nested/../..., and asserts every resulting container parameter is normalized.
    Verified to fail without the new extension (.../relative-paths/nested/../here.php vs
    .../relative-paths/here.php).
  • tests/PHPStan/Analyser/ResultCache/ResultCachePathTransformerTest.php (new) pins the invariant the
    canonicalization relies on: the relativize/absolutize round trip collapses .. segments, and
    absolutizeMeta() is that round trip's fixed point for analysedPaths, scannedFiles,
    executedFilesHashes, composerLocks and the install_path values inside composerInstalled.
  • e2e/result-cache-dots-in-path (new e2e fixture + step in e2e-tests.yml) reproduces the report:
    scanFiles, bootstrapFiles and a load-bearing stubFiles entry all written through a .. segment.
    Without the fixes the second run prints
    Result cache not used because the metadata do not match: scannedFiles, executedFilesHashes; with
    them it restores 0 files.
  • The result-cache-package-update e2e step gains an unchanged-run assertion that
    Composer metadata changed is not printed. Verified to fail before the getMeta() change.

Fixes phpstan/phpstan#15125

…he result cache meta

- Add `NormalizePathParametersExtension`, a compiler extension that runs `FileHelper::normalizePath()`
  over every parameter listed in the `expandRelativePaths` section once Nette has expanded the
  `%placeholder%`s. `NeonAdapter` skips values containing a `%`, so a `%rootDir%/../../../vendor/autoload.php`
  entry used to reach the container with its `..` segments intact.
- Canonicalize the freshly computed result cache meta in `ResultCacheManager::getMeta()` by putting it
  through `ResultCachePathTransformer::absolutizeMeta()`, the exact transformation the stored meta goes
  through on restore. Paths PHPStan does not control are not normalized to begin with - Composer records
  `install_path` as `vendor/composer/../foo/bar` - so `composerInstalled` used to differ on every single
  run. Bump `CACHE_VERSION` to `v15-canonicalPaths` and drop the now-redundant ad-hoc `configStubFiles`
  normalization.
- Normalize `--autoload-file` and `--configuration` in `CommandHelper::begin()`, the two siblings that
  only absolutized while `--generate-baseline`, `--tmp-file`, `--instead-of` and the analysed paths
  already normalized.
- Probed and found already correct: `FileExcluder`, `IgnoredErrorHelper`, `FileFinder`,
  `DefaultStubFilesProvider` and `NodeDependencies` all normalize defensively on their own, and
  `ReflectionClass::getFileName()` is resolved by PHP even for Composer's classmap entries.
注册 for free to join this conversation on GitHub. Already have an account? 登录 to comment

标签

None yet

项目

None yet

Development

Successfully merging this pull request may close these issues.

Relative paths in scanFiles lead to cache invalidation

1 participant