Resolve bootstrap autoloaders' classes from the files they ask for, running them for real only when that cannot redeclare a symbol - #6287
Merged
Conversation
…unning them for real only when that cannot redeclare a symbol The function_exists($className) gate added for phpstan/phpstan#14988 confused two meanings of "functions": AutoloadFunctionsSourceLocator is about autoload functions - the spl_autoload_register() callbacks that bootstrap files register - not about class names coinciding with defined functions. Executed for a class, such an autoloader either asks for a file, which the file-read trap detects, or defines the class without one, through class_alias() or eval(). The gate's probe also poisoned its own evidence: a trapped include succeeds with empty content, defining nothing but still registering in get_included_files(), so comparing the trapped files against get_included_files() taken after the probe declined the very lookup it was probing. Laravel's facade aliases whose target class is not loaded yet hit exactly that: resolving Redirect makes class_alias() ask for the file of Illuminate\Support\Facades\Redirect, the probe trapped that read, found the file "already included" and declined, and larastan's monicahq/monica e2e failed with seven "unknown class Redirect" errors. Any earlier probe trapping the same file poisoned later lookups the same way, since the pseudo-include sticks for the rest of the process. Now every lookup probes under the trap first, the class is located statically in the files the autoloaders asked for, and the autoloaders run for real only when executing those files could not fatally redeclare anything - judged by the files' statically parsed symbols, which pseudo-includes cannot poison, instead of by get_included_files(). The bug-15102c e2e covers the shape Illuminate\Foundation\AliasLoader creates in a real Laravel app: a prepended autoloader aliasing a name that collides with a global helper function to a class only Composer can autoload. See phpstan/phpstan#15102 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JkCjnojdWZvswKCPmzt4zY
Member
Author
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
function_exists($className)gate added for phpstan/phpstan#14988 confused two meanings of "functions":AutoloadFunctionsSourceLocatoris about autoload functions — thespl_autoload_register()callbacks that bootstrap files register — not about class names coinciding with defined functions. Executed for a class, such an autoloader either asks for a file, which the file-read trap detects, or defines the class without one, throughclass_alias()oreval().The gate's probe also poisoned its own evidence: a trapped include succeeds with empty content, defining nothing but still registering in
get_included_files(), so comparing the trapped files againstget_included_files()taken after the probe declined the very lookup it was probing. Laravel's facade aliases whose target class is not loaded yet hit exactly that: resolvingRedirectmakesclass_alias()ask for the file ofIlluminate\Support\Facades\Redirect, the probe trapped that read, found the file "already included" and declined — and larastan's monicahq/monica e2e failed with sevenunknown class Redirecterrors on 2.2.9 and current 2.2.x. Any earlier probe trapping the same file poisoned later lookups the same way, since the pseudo-include sticks for the rest of the process; include-list bookkeeping cannot be made sound.Now every class lookup through this locator works the same way:
AutoloadSourceLocator::locateIdentifierInFiles()) — a plain include-based autoloader resolves with no execution at all.wouldIncludingFilesRedeclareSymbols()says executing the located files cannot fatally redeclare a currently defined class or function. That is the actual #14988 hazard, judged by the files' statically parsed symbols, which pseudo-includes cannot poison.The new
e2e/bug-15102ccovers the shapeIlluminate\Foundation\AliasLoadercreates in a real Laravel app: a prepended autoloader aliasing a name that collides with a global helper function (Redirectvsredirect()) to a class only Composer can autoload. It fails on current 2.2.x and passes with this change.bug-15102,bug-15102b,bug-14988,bug-12972bandbug-12972cstill pass (verified with cleared result caches), and the full monicahq/monica + larastan analysis is clean against this branch.See phpstan/phpstan#15102
🤖 Generated with Claude Code
https://claude.ai/code/session_01JkCjnojdWZvswKCPmzt4zY