From 0c2657eb6cdd173285b703abe653a63dc158d173 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:42:42 +0000 Subject: [PATCH 1/6] Consult bootstrap-registered custom autoloaders only after the static source locators - Move `AutoloadFunctionsSourceLocator` in `BetterReflectionSourceLocatorFactory` from the front of the locator chain to just before the runtime `AutoloadSourceLocator`, so it runs only as a fallback after the analysed-file, Composer class-map/PSR-4 and PHP-internal locators. - Previously the custom autoloaders registered by bootstrap files (stored in `__phpstanAutoloadFunctions`, from which Composer's own ClassLoader is deliberately excluded) were invoked eagerly for every class name, even for classes that Composer resolves statically at runtime. Because Composer's loader is registered first at runtime, those custom autoloaders are never reached for such classes - PHPStan invoking them anyway ran code paths that cannot happen at runtime (e.g. an autoloader that throws for a class Composer already provides). - Classes that only a custom autoloader can produce (e.g. `eval`'d classes) are still handled, since the locator remains in the chain as a last resort. - Adds e2e test `e2e/bug-12972b` reproducing the reported crash and wires it into the e2e workflow. --- .github/workflows/e2e-tests.yml | 4 ++++ e2e/bug-12972b/.gitignore | 2 ++ e2e/bug-12972b/autoloader.php | 9 ++++++++ e2e/bug-12972b/composer.json | 7 ++++++ e2e/bug-12972b/phpstan.dist.neon | 8 +++++++ e2e/bug-12972b/real-world.php | 6 +++++ e2e/bug-12972b/src/folder/file2.php | 9 ++++++++ e2e/bug-12972b/src/other/file.php | 10 +++++++++ .../BetterReflectionSourceLocatorFactory.php | 22 +++++++++++++------ 9 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 e2e/bug-12972b/.gitignore create mode 100644 e2e/bug-12972b/autoloader.php create mode 100644 e2e/bug-12972b/composer.json create mode 100644 e2e/bug-12972b/phpstan.dist.neon create mode 100644 e2e/bug-12972b/real-world.php create mode 100644 e2e/bug-12972b/src/folder/file2.php create mode 100644 e2e/bug-12972b/src/other/file.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index a3a0d7e3ef4..e82799c53c6 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -128,6 +128,10 @@ jobs: cd e2e/bug-14514 composer install ../../bin/phpstan analyze bug-14515.php + - script: | + cd e2e/bug-12972b + composer install + ../../bin/phpstan analyze - script: | cd e2e/bug-14724 composer install diff --git a/e2e/bug-12972b/.gitignore b/e2e/bug-12972b/.gitignore new file mode 100644 index 00000000000..3a9875b460f --- /dev/null +++ b/e2e/bug-12972b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/bug-12972b/autoloader.php b/e2e/bug-12972b/autoloader.php new file mode 100644 index 00000000000..aaaae3ef08f --- /dev/null +++ b/e2e/bug-12972b/autoloader.php @@ -0,0 +1,9 @@ +doBar(new \other12972\MyClass()); diff --git a/e2e/bug-12972b/src/folder/file2.php b/e2e/bug-12972b/src/folder/file2.php new file mode 100644 index 00000000000..d81e18c01b5 --- /dev/null +++ b/e2e/bug-12972b/src/folder/file2.php @@ -0,0 +1,9 @@ +parser); - $locators[] = new AutoloadFunctionsSourceLocator( - new AutoloadSourceLocator($this->fileNodesFetcher, false), - new ReflectionClassSourceLocator( - $astLocator, - $this->reflectionSourceStubber, - ), - ); $analysedDirectories = []; $analysedFiles = []; @@ -184,6 +177,21 @@ public function create(): SourceLocator $this->phpVersion, )); + // Custom autoloaders registered by bootstrap files are consulted only + // as a fallback, after the static locators above (analysed files, + // Composer class map/PSR-4, PHP internals). At runtime Composer's + // class loader resolves such classes first, so their custom autoloaders + // are never invoked for them - invoking them eagerly here would run + // code paths that cannot happen at runtime. They remain useful for + // classes that only a custom autoloader can produce (e.g. eval'd). + $locators[] = new AutoloadFunctionsSourceLocator( + new AutoloadSourceLocator($this->fileNodesFetcher, false), + new ReflectionClassSourceLocator( + $astLocator, + $this->reflectionSourceStubber, + ), + ); + $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); $locators[] = new PhpVersionBlacklistSourceLocator(new PhpInternalSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); $locators[] = new PhpVersionBlacklistSourceLocator(new EvaledCodeSourceLocator($astLocator, $this->reflectionSourceStubber), $this->phpstormStubsSourceStubber); From bf2ce11cc151ba6c81dd541e6d34c7649a743a51 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 20 Jul 2026 16:55:48 +0200 Subject: [PATCH 2/6] move related lines in on block --- .../BetterReflection/BetterReflectionSourceLocatorFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index 1f5abf8c15e..afa7f21b0c7 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -97,8 +97,6 @@ public function create(): SourceLocator $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($this->singleReflectionFile); } - $astLocator = new Locator($this->parser); - $analysedDirectories = []; $analysedFiles = []; @@ -184,6 +182,7 @@ public function create(): SourceLocator // are never invoked for them - invoking them eagerly here would run // code paths that cannot happen at runtime. They remain useful for // classes that only a custom autoloader can produce (e.g. eval'd). + $astLocator = new Locator($this->parser); $locators[] = new AutoloadFunctionsSourceLocator( new AutoloadSourceLocator($this->fileNodesFetcher, false), new ReflectionClassSourceLocator( From dc2bac9289c2095a061cdb92da6dd2ccb78cc0fd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 20 Jul 2026 15:33:05 +0000 Subject: [PATCH 3/6] Consult bootstrap autoloaders before/after Composer based on registration order Bootstrap-registered custom autoloaders were all consulted after the static source locators. That mirrors runtime only for autoloaders registered after Composer's class loader; one registered before it (e.g. spl_autoload_register($fn, true, true)) runs before Composer at runtime and must be consulted before the static Composer locators too. Detect each autoloader's position relative to Composer's ClassLoader in the spl_autoload queue and split them into a prepended and an appended group. The prepended group is consulted before the static locators (front of the chain), the appended group only as a fallback after them. Co-Authored-By: Claude Opus 4.8 --- bin/phpstan | 34 ++----- src/Command/CommandHelper.php | 23 +++-- .../BetterReflectionSourceLocatorFactory.php | 30 ++++-- .../AutoloadFunctionsSourceLocator.php | 11 ++- src/autoloadFunctions.php | 85 +++++++++++++++++ .../CollectNewAutoloadFunctionsTest.php | 92 +++++++++++++++++++ 6 files changed, 229 insertions(+), 46 deletions(-) create mode 100644 tests/PHPStan/CollectNewAutoloadFunctionsTest.php diff --git a/bin/phpstan b/bin/phpstan index 217a147ac1b..15e67dce78d 100755 --- a/bin/phpstan +++ b/bin/phpstan @@ -91,31 +91,15 @@ use Symfony\Component\Console\Helper\ProgressBar; $autoloadFunctionsAfter = spl_autoload_functions(); if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) { - $newAutoloadFunctions = []; - foreach ($autoloadFunctionsAfter as $after) { - if ( - is_array($after) - && count($after) > 0 - ) { - if (is_object($after[0]) - && get_class($after[0]) === \Composer\Autoload\ClassLoader::class - ) { - continue; - } - if ($after[0] === 'PHPStan\\PharAutoloader') { - continue; - } - } - foreach ($autoloadFunctionsBefore as $before) { - if ($after === $before) { - continue 2; - } - } - - $newAutoloadFunctions[] = $after; - } - - $GLOBALS['__phpstanAutoloadFunctions'] = $newAutoloadFunctions; + $collectedAutoloadFunctions = \PHPStan\collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunctionsAfter); + $GLOBALS['__phpstanAutoloadFunctions'] = array_merge( + $GLOBALS['__phpstanAutoloadFunctions'] ?? [], + $collectedAutoloadFunctions['appended'], + ); + $GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] = array_merge( + $GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] ?? [], + $collectedAutoloadFunctions['prepended'], + ); } $devOrPharLoader->register(true); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index aa8a2a2210c..30cd4561dd2 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -41,6 +41,7 @@ use function array_filter; use function array_key_exists; use function array_map; +use function array_merge; use function array_values; use function class_exists; use function count; @@ -56,6 +57,7 @@ use function is_file; use function is_readable; use function is_string; +use function PHPStan\collectNewAutoloadFunctions; use function register_shutdown_function; use function spl_autoload_functions; use function sprintf; @@ -526,18 +528,15 @@ public static function begin( $autoloadFunctionsAfter = spl_autoload_functions(); if ($autoloadFunctionsBefore !== false && $autoloadFunctionsAfter !== false) { - $newAutoloadFunctions = $GLOBALS['__phpstanAutoloadFunctions'] ?? []; - foreach ($autoloadFunctionsAfter as $after) { - foreach ($autoloadFunctionsBefore as $before) { - if ($after === $before) { - continue 2; - } - } - - $newAutoloadFunctions[] = $after; - } - - $GLOBALS['__phpstanAutoloadFunctions'] = $newAutoloadFunctions; + $collectedAutoloadFunctions = collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunctionsAfter); + $GLOBALS['__phpstanAutoloadFunctions'] = array_merge( + $GLOBALS['__phpstanAutoloadFunctions'] ?? [], + $collectedAutoloadFunctions['appended'], + ); + $GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] = array_merge( + $GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] ?? [], + $collectedAutoloadFunctions['prepended'], + ); } if (PHP_VERSION_ID >= 80000) { diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index afa7f21b0c7..2a97a8e4efd 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -97,6 +97,21 @@ public function create(): SourceLocator $locators[] = $this->optimizedSingleFileSourceLocatorRepository->getOrCreate($this->singleReflectionFile); } + $astLocator = new Locator($this->parser); + + // Custom autoloaders that bootstrap files registered *before* Composer's + // class loader (e.g. spl_autoload_register($fn, true, true)) run before + // Composer resolves the class at runtime, so they are consulted before + // the static source locators below - mirroring that runtime order. + $locators[] = new AutoloadFunctionsSourceLocator( + new AutoloadSourceLocator($this->fileNodesFetcher, false), + new ReflectionClassSourceLocator( + $astLocator, + $this->reflectionSourceStubber, + ), + true, + ); + $analysedDirectories = []; $analysedFiles = []; @@ -175,14 +190,13 @@ public function create(): SourceLocator $this->phpVersion, )); - // Custom autoloaders registered by bootstrap files are consulted only - // as a fallback, after the static locators above (analysed files, - // Composer class map/PSR-4, PHP internals). At runtime Composer's - // class loader resolves such classes first, so their custom autoloaders - // are never invoked for them - invoking them eagerly here would run - // code paths that cannot happen at runtime. They remain useful for - // classes that only a custom autoloader can produce (e.g. eval'd). - $astLocator = new Locator($this->parser); + // Custom autoloaders registered *after* Composer's class loader are + // consulted only as a fallback, after the static locators above + // (analysed files, Composer class map/PSR-4, PHP internals). At runtime + // Composer's class loader resolves such classes first, so these custom + // autoloaders are never invoked for them - invoking them eagerly here + // would run code paths that cannot happen at runtime. They remain useful + // for classes that only a custom autoloader can produce (e.g. eval'd). $locators[] = new AutoloadFunctionsSourceLocator( new AutoloadSourceLocator($this->fileNodesFetcher, false), new ReflectionClassSourceLocator( diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php index 3da44c64bf1..08807a281c7 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php @@ -11,14 +11,21 @@ use function class_exists; use function interface_exists; use function PHPStan\autoloadFunctions; +use function PHPStan\autoloadFunctionsPrependedToComposer; use function trait_exists; final class AutoloadFunctionsSourceLocator implements SourceLocator { + /** + * @param bool $prependedToComposer When true, consult only the autoloaders + * registered before Composer's class loader (prepended); otherwise consult + * the ones registered after it (appended). + */ public function __construct( private AutoloadSourceLocator $autoloadSourceLocator, private ReflectionClassSourceLocator $reflectionClassSourceLocator, + private bool $prependedToComposer = false, ) { } @@ -35,7 +42,9 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier): return null; } - $autoloadFunctions = autoloadFunctions(); + $autoloadFunctions = $this->prependedToComposer + ? autoloadFunctionsPrependedToComposer() + : autoloadFunctions(); foreach ($autoloadFunctions as $autoloadFunction) { $autoloadFunction($className); $reflection = $this->autoloadSourceLocator->locateIdentifier($reflector, $identifier); diff --git a/src/autoloadFunctions.php b/src/autoloadFunctions.php index 3239fe6aff6..617d7f7ee53 100644 --- a/src/autoloadFunctions.php +++ b/src/autoloadFunctions.php @@ -2,10 +2,95 @@ namespace PHPStan; +use Composer\Autoload\ClassLoader; +use function count; +use function get_class; +use function is_array; +use function is_object; + /** + * Autoloaders that were registered *after* Composer's class loader in the + * spl_autoload queue. At runtime Composer resolves the class first, so these + * are consulted only as a fallback, after the static source locators. + * * @return array */ function autoloadFunctions(): array // phpcs:ignore Squiz.Functions.GlobalFunction.Found { return $GLOBALS['__phpstanAutoloadFunctions'] ?? []; } + +/** + * Autoloaders that were registered *before* Composer's class loader in the + * spl_autoload queue (e.g. spl_autoload_register($fn, true, true)). At runtime + * these run before Composer resolves the class, so they must be consulted + * before the static Composer source locators to mirror that order. + * + * @return array + */ +function autoloadFunctionsPrependedToComposer(): array // phpcs:ignore Squiz.Functions.GlobalFunction.Found +{ + return $GLOBALS['__phpstanAutoloadFunctionsPrependedToComposer'] ?? []; +} + +/** + * Splits the autoload functions registered while loading Composer's autoloader + * and the bootstrap files into those registered before and after Composer's own + * class loader in the spl_autoload queue. This lets PHPStan consult them in the + * same order relative to Composer as PHP does at runtime, instead of always + * invoking them before (or after) the static Composer source locators. + * + * @param list|false $autoloadFunctionsBefore + * @param list|false $autoloadFunctionsAfter + * @return array{prepended: list, appended: list} + */ +function collectNewAutoloadFunctions($autoloadFunctionsBefore, $autoloadFunctionsAfter): array // phpcs:ignore Squiz.Functions.GlobalFunction.Found +{ + $prepended = []; + $appended = []; + + if ($autoloadFunctionsBefore === false || $autoloadFunctionsAfter === false) { + return ['prepended' => $prepended, 'appended' => $appended]; + } + + $composerIndex = null; + foreach ($autoloadFunctionsAfter as $index => $after) { + if ( + is_array($after) + && count($after) > 0 + && is_object($after[0]) + && get_class($after[0]) === ClassLoader::class + ) { + $composerIndex = $index; + break; + } + } + + foreach ($autoloadFunctionsAfter as $index => $after) { + if (is_array($after) && count($after) > 0) { + if ( + is_object($after[0]) + && get_class($after[0]) === ClassLoader::class + ) { + continue; + } + if ($after[0] === 'PHPStan\\PharAutoloader') { + continue; + } + } + + foreach ($autoloadFunctionsBefore as $before) { + if ($after === $before) { + continue 2; + } + } + + if ($composerIndex !== null && $index < $composerIndex) { + $prepended[] = $after; + } else { + $appended[] = $after; + } + } + + return ['prepended' => $prepended, 'appended' => $appended]; +} diff --git a/tests/PHPStan/CollectNewAutoloadFunctionsTest.php b/tests/PHPStan/CollectNewAutoloadFunctionsTest.php new file mode 100644 index 00000000000..9e2c18f551c --- /dev/null +++ b/tests/PHPStan/CollectNewAutoloadFunctionsTest.php @@ -0,0 +1,92 @@ +assertSame( + ['prepended' => [], 'appended' => []], + collectNewAutoloadFunctions(false, false), + ); + + $after = [static function (string $class): void { + }]; + $this->assertSame( + ['prepended' => [], 'appended' => []], + collectNewAutoloadFunctions(false, $after), + ); + } + + public function testAutoloadersAreSplitByComposerPosition(): void + { + $prepended = static function (string $class): void { + }; + $composer = new ClassLoader(); + $appended = static function (string $class): void { + }; + + $before = []; + $after = [$prepended, [$composer, 'loadClass'], $appended]; + + $result = collectNewAutoloadFunctions($before, $after); + + $this->assertSame([$prepended], $result['prepended']); + $this->assertSame([$appended], $result['appended']); + } + + public function testWithoutComposerEverythingIsAppended(): void + { + $first = static function (string $class): void { + }; + $second = static function (string $class): void { + }; + + $result = collectNewAutoloadFunctions([], [$first, $second]); + + $this->assertSame([], $result['prepended']); + $this->assertSame([$first, $second], $result['appended']); + } + + public function testComposerAndPharAutoloaderAndPreexistingAreExcluded(): void + { + $preexisting = static function (string $class): void { + }; + $composer = new ClassLoader(); + $bootstrap = static function (string $class): void { + }; + + $before = [$preexisting]; + $after = [$preexisting, [$composer, 'loadClass'], ['PHPStan\\PharAutoloader', 'loadClass'], $bootstrap]; + + $result = collectNewAutoloadFunctions($before, $after); + + $this->assertSame([], $result['prepended']); + $this->assertSame([$bootstrap], $result['appended']); + } + + public function testPreexistingAutoloaderBeforeComposerIsNotReported(): void + { + $preexisting = static function (string $class): void { + }; + $composer = new ClassLoader(); + $prependedBootstrap = static function (string $class): void { + }; + + // $preexisting was registered before PHPStan loaded the project - it must + // be ignored even though it sits before Composer in the queue. + $before = [$preexisting]; + $after = [$preexisting, $prependedBootstrap, [$composer, 'loadClass']]; + + $result = collectNewAutoloadFunctions($before, $after); + + $this->assertSame([$prependedBootstrap], $result['prepended']); + $this->assertSame([], $result['appended']); + } + +} From 07d27ccf39d6f5e4e191192a57ee3104c53823a0 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 20 Jul 2026 15:33:10 +0000 Subject: [PATCH 4/6] Add e2e project for a prepended bootstrap autoloader bug-12972c registers a bootstrap autoloader before Composer's class loader that resolves a class Composer also maps, but with a different API. At runtime the prepended autoloader wins; PHPStan now reflects the same version instead of Composer's, so analysis matches runtime. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/e2e-tests.yml | 4 ++++ e2e/bug-12972c/.gitignore | 2 ++ e2e/bug-12972c/autoloader.php | 12 ++++++++++++ e2e/bug-12972c/classmap/Thing.php | 10 ++++++++++ e2e/bug-12972c/composer.json | 7 +++++++ e2e/bug-12972c/consumer/consumer.php | 10 ++++++++++ e2e/bug-12972c/phpstan.dist.neon | 8 ++++++++ e2e/bug-12972c/prepended/Thing.php | 10 ++++++++++ e2e/bug-12972c/real-world.php | 6 ++++++ 9 files changed, 69 insertions(+) create mode 100644 e2e/bug-12972c/.gitignore create mode 100644 e2e/bug-12972c/autoloader.php create mode 100644 e2e/bug-12972c/classmap/Thing.php create mode 100644 e2e/bug-12972c/composer.json create mode 100644 e2e/bug-12972c/consumer/consumer.php create mode 100644 e2e/bug-12972c/phpstan.dist.neon create mode 100644 e2e/bug-12972c/prepended/Thing.php create mode 100644 e2e/bug-12972c/real-world.php diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index e82799c53c6..ebfb5423637 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -132,6 +132,10 @@ jobs: cd e2e/bug-12972b composer install ../../bin/phpstan analyze + - script: | + cd e2e/bug-12972c + composer install + ../../bin/phpstan analyze - script: | cd e2e/bug-14724 composer install diff --git a/e2e/bug-12972c/.gitignore b/e2e/bug-12972c/.gitignore new file mode 100644 index 00000000000..3a9875b460f --- /dev/null +++ b/e2e/bug-12972c/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +composer.lock diff --git a/e2e/bug-12972c/autoloader.php b/e2e/bug-12972c/autoloader.php new file mode 100644 index 00000000000..0eabb486506 --- /dev/null +++ b/e2e/bug-12972c/autoloader.php @@ -0,0 +1,12 @@ +prependedOnly(); +} diff --git a/e2e/bug-12972c/phpstan.dist.neon b/e2e/bug-12972c/phpstan.dist.neon new file mode 100644 index 00000000000..c9f081939c2 --- /dev/null +++ b/e2e/bug-12972c/phpstan.dist.neon @@ -0,0 +1,8 @@ +parameters: + level: 9 + + paths: + - consumer + + bootstrapFiles: + - autoloader.php diff --git a/e2e/bug-12972c/prepended/Thing.php b/e2e/bug-12972c/prepended/Thing.php new file mode 100644 index 00000000000..b97d6a4750d --- /dev/null +++ b/e2e/bug-12972c/prepended/Thing.php @@ -0,0 +1,10 @@ +prependedOnly(), "\n"; From a33563b094b1bfb400dd4d94e5cc169fd171b7cd Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 20 Jul 2026 15:49:40 +0000 Subject: [PATCH 5/6] Require explicit $prependedToComposer argument on AutoloadFunctionsSourceLocator Callers now pass the boolean explicitly instead of relying on a default, making the prepended/appended intent visible at each construction site. Co-Authored-By: Claude Opus 4.8 --- .../BetterReflection/BetterReflectionSourceLocatorFactory.php | 1 + .../SourceLocator/AutoloadFunctionsSourceLocator.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php index 2a97a8e4efd..055df460810 100644 --- a/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php +++ b/src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php @@ -203,6 +203,7 @@ public function create(): SourceLocator $astLocator, $this->reflectionSourceStubber, ), + false, ); $locators[] = new AutoloadSourceLocator($this->fileNodesFetcher, true); diff --git a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php index 08807a281c7..30ab9fa2d1f 100644 --- a/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php +++ b/src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php @@ -25,7 +25,7 @@ final class AutoloadFunctionsSourceLocator implements SourceLocator public function __construct( private AutoloadSourceLocator $autoloadSourceLocator, private ReflectionClassSourceLocator $reflectionClassSourceLocator, - private bool $prependedToComposer = false, + private bool $prependedToComposer, ) { } From 635b06f013506d99e27b3fc66a9ee901ece110d1 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 24 Jul 2026 08:45:30 +0000 Subject: [PATCH 6/6] Un-prefix Composer\Autoload\ClassLoader in scoped src/autoloadFunctions.php The Composer ClassLoader identity check used to exclude Composer's own class loader from the bootstrap autoloaders PHPStan invokes for real was moved from bin/phpstan into src/autoloadFunctions.php. bin/phpstan is on the scoper whitelist that reverts Composer\Autoload\ClassLoader back to its un-prefixed name, but src/autoloadFunctions.php was not, so in the PHAR the check compared against _PHPStan_xxx\Composer\Autoload\ClassLoader. The analysed project's real Composer loader is un-prefixed and therefore no longer matched, so it was collected as a bootstrap autoload function and invoked for real - require-ing files (e.g. Roave/BetterReflection's invalid inheritance fixtures) and hard-crashing analysis with a fatal that cannot happen at runtime. Co-Authored-By: Claude Opus 4.8 --- compiler/build/scoper.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/build/scoper.inc.php b/compiler/build/scoper.inc.php index f6e29aa24e7..adb2da3eead 100644 --- a/compiler/build/scoper.inc.php +++ b/compiler/build/scoper.inc.php @@ -141,6 +141,7 @@ function (string $filePath, string $prefix, string $content): string { function (string $filePath, string $prefix, string $content): string { if (!in_array($filePath, [ 'bin/phpstan', + 'src/autoloadFunctions.php', 'src/Testing/TestCaseSourceLocatorFactory.php', 'src/Testing/PHPStanTestCase.php', 'vendor/ondrejmirtes/better-reflection/src/SourceLocator/Type/ComposerSourceLocator.php',