Skip to content

Do not resolve include paths through a stream wrapper that is not registered - #6397

Merged
ondrejmirtes merged 2 commits into
2.2.xfrom
include-path-stream-wrapper
Sep 8, 2026
Merged

ondrejmirtes merged 2 commits into
2.2.xfrom
include-path-stream-wrapper

Conversation

@ondrejmirtes

@ondrejmirtes ondrejmirtes commented Sep 8, 2026 •

Copy link
Copy Markdown
Member

Analysing Drupal core prints this before the analysis starts, on every run:

PHP Warning:  is_file(): Unable to find the wrapper "vfs" - did you forget to enable it when you configured PHP? in .../ResultCacheManager.php on line 739

How it happens

Drupal core has a literal include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php' in core/tests/Drupal/Tests/Core/Update/UpdateRegistryTest.php. vfsStream registers that wrapper from a test's setUp(), which never runs inside PHPStan, so is_file() on the path cannot answer - it raises the warning and returns false.

FileHelper::absolutizePath() recognises scheme:// and returns such a path unchanged, so IncludedFilePathResolver::resolve() handed it back as a filesystem candidate. Three callers then stat it:

  1. RequireFileExistsRule::doesFileExist() - quiet, FileAnalyser installs an error handler that swallows warnings during the analysis.
  2. DependencyResolver's Include_ branch - also quiet, and its own is_file() guard means the path is not recorded there.
  3. The one that shows: the rule attaches its candidate paths to the error as a FileDependenciesRuleError, so the verdict is rechecked if the file ever appears. That lands in the dependency graph, the result cache stores it with MISSING_FILE_HASH, and every restore stats it again - outside any error handler.

The fix

A path whose stream wrapper is not registered has no filesystem candidates, and cannot come to have any, so resolve() returns none for it.

The include is still reported as not found - PHPStan cannot see through the wrapper either way - it just no longer gets there by stat'ing, and no longer leaves behind a dependency that every restore stats again.

Verification

On Drupal core: 8 warnings -> 0 on both a cold and a warm run, analysis output byte-identical (the same 22 pre-existing ignore.unmatched errors).

Note for reviewers: a RuleTestCase cannot catch this - FileAnalyser's error handler eats the warning, so a rule test passes with and without the fix. The regression test is a unit test on the resolver; it fails before the fix with the path still in the candidate list.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDcVRMjAXM7WQGSzysw7WH

Closes phpstan/phpstan#15192

ondrejmirtes and others added 2 commits September 8, 2026 13:23
…istered

`include_once 'vfs://drupal/sites/default/modules/module_a/module_a.post_update.php'` - Drupal
core has one in UpdateRegistryTest - names a stream wrapper, not a place on the filesystem.
vfsStream registers that wrapper from a test's setUp(), which never runs inside PHPStan, so
is_file() on the path cannot answer: it raises "Unable to find the wrapper" and returns false.

IncludedFilePathResolver handed the path back as a candidate anyway, and three callers stat'd
it. Two are quiet - FileAnalyser's error handler swallows the warning during the analysis - but
the third is not: RequireFileExistsRule records the candidates on its error so that the verdict
is rechecked when the file appears, the result cache stores that as a missing file dependency,
and every warm run then prints

    PHP Warning: is_file(): Unable to find the wrapper "vfs" ... in ResultCacheManager.php

twice, before the analysis starts.

A path whose wrapper is not registered has no filesystem candidates, and cannot come to have
any, so resolve() now returns none for it. The include is still reported as not found - PHPStan
cannot see through the wrapper either way - it just no longer gets there by stat'ing, and no
longer leaves a dependency behind that every restore stats again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDcVRMjAXM7WQGSzysw7WH
absolutizePath() joins with DIRECTORY_SEPARATOR, so on Windows the resolver returns
`D:\...\File\lorem.php` while the expectation built from `__DIR__ . '/lorem.php'` kept the
forward slash and the assertion could not match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDcVRMjAXM7WQGSzysw7WH
@ondrejmirtes
ondrejmirtes merged commit 4d699ef into 2.2.x Sep 8, 2026
460 of 465 checks passed
@ondrejmirtes
ondrejmirtes deleted the include-path-stream-wrapper branch September 8, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant