Decline a bootstrap autoloader only when it would re-include a loaded file - #6265
Open
SanderMuller wants to merge 1 commit into
Open
Decline a bootstrap autoloader only when it would re-include a loaded file#6265SanderMuller wants to merge 1 commit into
SanderMuller wants to merge 1 commit into
Conversation
SanderMuller
force-pushed
the
autoloader-function-name-collision
branch
3 times, most recently
from
August 25, 2026 07:57
db6ef28 to
dae2292
Compare
… file The guard added for phpstan/phpstan#14988 declined this locator whenever a function of the class's name existed. Classes and functions live in separate symbol spaces, so that also blocked class names which merely coincide with a function - Laravel's facade aliases are exactly that shape, since Cache, File, Str and friends coincide with the global helpers cache(), file() and str(), and `use Cache;` started reporting class.notFound. The hazard was never the name: it was a catch-all autoloader resolving a class name to the function's own file and including it a second time. Probing the autoloaders under the file-read trap says which file they would read without executing it, so only that case declines. A loader that defines the class without reading a file - class_alias(), eval() - now runs as it did before. Closes phpstan/phpstan#15102 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SanderMuller
force-pushed
the
autoloader-function-name-collision
branch
from
August 25, 2026 08:05
dae2292 to
01f2943
Compare
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.
Fixes the second regression reported in phpstan/phpstan#15102 - the one @hoetaek reduced to a portable repro. This is not the ordering issue that opened that ticket; that one is #6069's heuristic and needs its own change.
What was wrong
#6185 added this to
AutoloadFunctionsSourceLocator:Classes and functions occupy separate symbol spaces, so a class name coinciding with a function name is legal and common. Laravel is the worst case: the facade aliases
Cache,File,Str,Hashcoincide with the global helperscache(),str()and with PHP's ownfile(),hash().Illuminate\Foundation\AliasLoadercreates those aliases lazily from a prepended autoloader, so with the guard in placeuse Cache;reportsclass.notFoundfor every larastan user. Verified against the release phars on @hoetaek's repro:File(collides withfile())Alias(no such function)What the hazard actually was
phpstan/phpstan#14988 was not about the name. A catch-all autoloader - PHP_CodeSniffer's, falling back to Composer's
findFile()- resolved a class name to a function's file and plain-included it a second time, fatally redeclaring the function.So the check now asks that question instead of guessing from the name: probe the autoloaders under
FileReadTrapStreamWrapper, which reports which file they would read without executing it, and decline only when that file is already inget_included_files(). An autoloader that defines the class without reading a file -class_alias(),eval()- runs exactly as it did in 2.2.8. The probe only runs for names that actually collide with a function, and not at all when the bucket holds no autoloaders - so a project without bootstrap autoloaders never reaches it: analysingsrc/Rulestriggers 0 probes. Where it does run,get_included_files()held ~35 entries, so the scan is nothing.One subtlety the trap forces: it intercepts file reads, not execution, so the probe really does run the autoloaders. An autoloader that defines the class without reading a file has therefore already done its work by the time the probe returns, and calling it again would redeclare what it defined -
class_alias()warns that the name is already in use. So when the class exists after the probe, the locator reflects it directly instead of looping over the autoloaders again; the test asserts the autoloader runs exactly once. For the same reason the probe stops at the first autoloader that defines the name, the wayspl_autoload_call()does - otherwise a later catch-all autoloader in the same bucket could still resolve that name to a loaded file and veto a class the first one had already defined.Verification
AutoloadFunctionsSourceLocatorTestpins two cases, both verified failing without the change: the alias case (on2.2.xthe locator declines and returns null), and a defining autoloader followed by a catch-all one in the same bucket. It also asserts the autoloader is invoked exactly once - the outcome assertion alone passes either way, only the count (2 vs 1) separates them.e2e/bug-14988still exits 0 - the redeclare fatal does not come back, it is prevented by the trap rather than by the name.e2e/bug-12972b,e2e/bug-12972c(Consult bootstrap-registered custom autoloaders only after the static source locators #6069) andCollectNewAutoloadFunctionsTestunchanged and green.2.2.x.2.2.xbreakage, not this branch: the same 13 - integration tests,phpstan-doctrineon all four PHP versions (Call to deprecated method toMutatingScope()),Test (PHP 8.5),Result cache E2E bug-11826,Check for typos(which fails on the tip of2.2.xitself overEXPECTED_EXTENSION_VERSION = '873ede9') - are red on every PR rebased today, e.g. Implement worker auto-scaler (stability and performance boost) #6256 and Run bootstrapFiles per forked worker instead of inheriting the parent's #6264. The one extra, WindowsIntersectionTypeTest::testIsAcceptedBy, is the toggle leak Isolate intersection test toggle state #6255 fixes.On coverage, one thing worth knowing: an
e2e/*project cannot pin this. Those run../../bin/phpstanfrom source, and in a source run the class is rescued further down the locator chain, so the same project passes with and without the guard - only the phar exhibits it end-to-end. That is why the regression test is at unit level, where the locator is exercised directly and the outcome does not depend on how PHPStan itself was loaded. If a phar-based e2e lane is wanted, that is worth doing on its own for this whole area, since the ordering half of #15102 has the same problem.