Skip to content

Do not let a bootstrap autoloader's exception abort the analysis - #6294

Open
SanderMuller wants to merge 1 commit into
phpstan:2.2.xfrom
SanderMuller:autoload-swallow-autoloader-exceptions
Open

Do not let a bootstrap autoloader's exception abort the analysis#6294
SanderMuller wants to merge 1 commit into
phpstan:2.2.xfrom
SanderMuller:autoload-swallow-autoloader-exceptions

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

After #6292 reverted the whole bootstrap-autoloader line, this is a fresh, much smaller attempt at one half
of what was reopened. It changes neither the order of the source locators nor the way autoloaders are run,
and it declines nothing by name - the three things that kept breaking other people's projects.

The problem. Both source locators that consult the registered autoloaders walk the entire spl queue.
That is not the order PHP uses: at runtime the class loader that resolves the class first means the other
autoloaders are never invoked for that name at all. An autoloader that throws when the file it found does
not declare the class it was asked for therefore throws here for names it can never see at runtime, and the
exception surfaces as Internal error: ..., aborting the file.

Two widely used autoloaders do exactly that:

  • symfony/error-handler's DebugClassLoader - RuntimeException in four conditions: case mismatch
    between the loaded and the declared name, a name containing /, "the autoloader expected class X to be
    defined in file Y", and case mismatch between the class and the real file name.
  • Yii 2's BaseYii::autoload() - UnknownClassException when the file it included did not declare the
    class and YII_DEBUG is on. It also plain-includes, so it carries the Cannot redeclare function: AutoloadFunctionsSourceLocator includes PSR-4 function files untrapped phpstan#14988 shape too.

The fix swallows the exception so the remaining autoloaders and source locators get their turn: the class
is then resolved the way it is at runtime, or reported as not found. Both call sites are fixed - the loop in
AutoloadSourceLocator::locateClassByName() that runs the autoloaders inside the file-read trap has the same
defect and predates the reverted work, and fixing only one leaves every name no static locator resolves
still aborting.

Measured against the guard corpus from #6293 (thanks for merging it - this is what makes the change
bounded rather than another guess):

e2e project 2.2.x today this branch
bug-12972b (phpstan/phpstan#14976) Internal error No errors
debug-class-loader (new, real symfony/error-handler) Internal error No errors
bug-14988 redeclare fatal redeclare fatal - unchanged
bug-12972c pass pass
bug-15102, bug-15102b, bug-15102c pass pass
class-alias-loader, robot-loader pass pass

e2e/bug-12972b is re-enabled in e2e-tests.yml; e2e/bug-14988 stays commented out.

What this does not fix. phpstan/phpstan#14988 is untouched. Its real-world carrier is
squizlabs/php_codesniffer, whose autoloader falls back to Composer's findFile() and then plain-includes
the result, with a loaded-files cache that cannot know about files Composer's files autoload already
loaded. That is a fatal error, not an exception, so no catch can reach it.

No Closes for phpstan/phpstan#14976 either, deliberately. The autoloader is still invoked out of
runtime order and its side effects still happen; only the internal error goes away. Whether that is enough
to close the issue is your call.

Objection worth answering up front: swallowing hides a signal. The strongest case is DebugClassLoader's
case-mismatch throw - and PHPStan reports that itself, via class.nameCase, so the information is not lost.

Performance: no new calls; a try/catch costs nothing on the path where nothing throws, and
autoloadFunctions() is empty unless the project registers autoloaders from bootstrapFiles.

Three unit tests, each verified failing before the change. Full suite, self-analysis and phpcs green.

Both source locators that ask the registered autoloaders for a class walk the
whole spl queue, which is not the order PHP uses: at runtime the class loader
that resolves the class first means the other autoloaders are never invoked for
that name. An autoloader that throws when the file it found does not declare
the class it was asked for therefore throws here for names it can never see at
runtime, and the exception surfaces as an internal error that aborts the file.
Two widely used ones do that - symfony/error-handler's DebugClassLoader, which
throws a RuntimeException in four conditions, and Yii 2's Yii::autoload(),
which throws an UnknownClassException when the file it included did not declare
the class and YII_DEBUG is on.

Swallowing it lets the remaining autoloaders and source locators take their
turn, so the class is resolved the way it is at runtime, or reported as not
found. The trapped loop in AutoloadSourceLocator has the same defect; it
predates the recently reverted work and is fixed here too, since fixing only
one of the two leaves any name no static locator resolves still aborting.

Unlike the reverted attempts this changes neither the order of the locators nor
the way autoloaders are run, and it declines nothing by name.

e2e/debug-class-loader is the DebugClassLoader case with the real package: an
internal error before, the unknown class reported after. e2e/bug-12972b, the
project phpstan/phpstan#14976 is about, is re-enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@SanderMuller
SanderMuller force-pushed the autoload-swallow-autoloader-exceptions branch from b5a6d39 to 43f6dde Compare August 27, 2026 21:30
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