Skip to content

Commit 95c1b80

Browse files
authored
Do not let a bootstrap autoloader's exception abort the analysis (#6294)
1 parent 96c559d commit 95c1b80

14 files changed

Lines changed: 333 additions & 12 deletions

File tree

‎.github/workflows/e2e-tests.yml‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,15 +219,24 @@ jobs:
219219
cd e2e/bug-14514
220220
composer install
221221
../../bin/phpstan analyze bug-14515.php
222-
# Fails since the revert of the bootstrap autoloader changes:
222+
# Fails until https://github.com/phpstan/phpstan/issues/14988 is fixed again:
223223
# - script: |
224224
# cd e2e/bug-14988
225225
# composer install
226226
# ../../bin/phpstan analyse
227-
# - script: |
228-
# cd e2e/bug-12972b
229-
# composer install
230-
# ../../bin/phpstan analyze
227+
- script: |
228+
cd e2e/bug-12972b
229+
composer install
230+
../../bin/phpstan analyze
231+
- script: |
232+
cd e2e/debug-class-loader
233+
composer install
234+
../../bin/phpstan analyze
235+
# With --debug, the analysis runs in the main process, and PHPStan prints the
236+
# exception that DebugClassLoader threw for the unknown class.
237+
OUTPUT=$(../bashunit assert exit_code "0" "../../bin/phpstan analyze --debug")
238+
echo "$OUTPUT"
239+
../bashunit assert contains 'by a bootstrap autoloader for class DebugClassLoaderE2e\Widget: The autoloader expected class' "$OUTPUT"
231240
- script: |
232241
cd e2e/bug-12972c
233242
composer install

‎e2e/debug-class-loader/.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
/composer.lock
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php declare(strict_types = 1);
2+
3+
// What a Symfony application's entry script does in dev: DebugClassLoader wraps every
4+
// registered autoloader and throws a RuntimeException when the file it found does not
5+
// declare the class it was asked for, or when the case does not match.
6+
require_once __DIR__ . '/vendor/autoload.php';
7+
8+
Symfony\Component\ErrorHandler\DebugClassLoader::enable();
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"require": {
3+
"symfony/error-handler": "^7.0"
4+
},
5+
"autoload": {
6+
"psr-4": {
7+
"DebugClassLoaderE2e\\": "src/"
8+
}
9+
}
10+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
parameters:
2+
level: 6
3+
bootstrapFiles:
4+
- bootstrap.php
5+
paths:
6+
- test.php
7+
ignoreErrors:
8+
# The point of this project is that analysis completes and reports the unknown class
9+
# instead of dying inside the autoloader.
10+
-
11+
message: '#^Parameter \$widget of method DebugClassLoaderE2e\\Consumer\:\:doFoo\(\) has invalid type DebugClassLoaderE2e\\Widget\.$#'
12+
path: test.php
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DebugClassLoaderE2e;
4+
5+
// The declared name does not match the file name, so Composer's PSR-4 map sends
6+
// DebugClassLoaderE2e\Widget to this file and DebugClassLoader throws for it.
7+
class Widgets
8+
{
9+
10+
}

‎e2e/debug-class-loader/test.php‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace DebugClassLoaderE2e;
4+
5+
class Consumer
6+
{
7+
8+
// Nothing ever instantiates the mistyped name, so no autoloader is invoked for it at
9+
// runtime. PHPStan resolves every name it sees, so it does ask - and the exception used
10+
// to abort the analysis of this file with an internal error instead of reporting the
11+
// unknown class. See https://github.com/phpstan/phpstan/issues/14976
12+
/** @param Widget $widget */
13+
public function doFoo($widget): void
14+
{
15+
}
16+
17+
}

‎src/Command/BootstrapFilesRunner.php‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PHPStan\DependencyInjection\AutowiredService;
66
use PHPStan\DependencyInjection\Container;
7+
use PHPStan\Reflection\BetterReflection\SourceLocator\BootstrapAutoloaderExceptionReporter;
78
use function spl_autoload_functions;
89

910
/**
@@ -32,7 +33,10 @@ final class BootstrapFilesRunner
3233

3334
private bool $hasRun = false;
3435

35-
public function __construct(private Container $container)
36+
public function __construct(
37+
private Container $container,
38+
private BootstrapAutoloaderExceptionReporter $bootstrapAutoloaderExceptionReporter,
39+
)
3640
{
3741
}
3842

@@ -41,6 +45,10 @@ public function __construct(private Container $container)
4145
*/
4246
public function run(Output $errorOutput, bool $debugEnabled): void
4347
{
48+
if ($debugEnabled) {
49+
$this->bootstrapAutoloaderExceptionReporter->enable($errorOutput);
50+
}
51+
4452
if ($this->hasRun) {
4553
return;
4654
}

‎src/Reflection/BetterReflection/BetterReflectionSourceLocatorFactory.php‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Php\PhpVersion;
2020
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadFunctionsSourceLocator;
2121
use PHPStan\Reflection\BetterReflection\SourceLocator\AutoloadSourceLocator;
22+
use PHPStan\Reflection\BetterReflection\SourceLocator\BootstrapAutoloaderExceptionReporter;
2223
use PHPStan\Reflection\BetterReflection\SourceLocator\CachedPhpInternalSourceLocator;
2324
use PHPStan\Reflection\BetterReflection\SourceLocator\ComposerJsonAndInstalledJsonSourceLocatorMaker;
2425
use PHPStan\Reflection\BetterReflection\SourceLocator\FileNodesFetcher;
@@ -69,6 +70,7 @@ public function __construct(
6970
private ComposerJsonAndInstalledJsonSourceLocatorMaker $composerJsonAndInstalledJsonSourceLocatorMaker,
7071
private OptimizedPsrAutoloaderLocatorFactory $optimizedPsrAutoloaderLocatorFactory,
7172
private FileNodesFetcher $fileNodesFetcher,
73+
private BootstrapAutoloaderExceptionReporter $bootstrapAutoloaderExceptionReporter,
7274
#[AutowiredParameter]
7375
private array $scanFiles,
7476
#[AutowiredParameter]
@@ -109,6 +111,7 @@ public function create(): SourceLocator
109111
$astLocator,
110112
$this->reflectionSourceStubber,
111113
),
114+
$this->bootstrapAutoloaderExceptionReporter,
112115
));
113116

114117
$analysedDirectories = [];

‎src/Reflection/BetterReflection/SourceLocator/AutoloadFunctionsSourceLocator.php‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PHPStan\BetterReflection\Reflection\Reflection;
99
use PHPStan\BetterReflection\Reflector\Reflector;
1010
use PHPStan\BetterReflection\SourceLocator\Type\SourceLocator;
11+
use Throwable;
1112
use function class_exists;
1213
use function interface_exists;
1314
use function PHPStan\autoloadFunctions;
@@ -19,6 +20,7 @@ final class AutoloadFunctionsSourceLocator implements SourceLocator
1920
public function __construct(
2021
private AutoloadSourceLocator $autoloadSourceLocator,
2122
private ReflectionClassSourceLocator $reflectionClassSourceLocator,
23+
private BootstrapAutoloaderExceptionReporter $exceptionReporter,
2224
)
2325
{
2426
}
@@ -37,7 +39,22 @@ public function locateIdentifier(Reflector $reflector, Identifier $identifier):
3739

3840
$autoloadFunctions = autoloadFunctions();
3941
foreach ($autoloadFunctions as $autoloadFunction) {
40-
$autoloadFunction($className);
42+
try {
43+
$autoloadFunction($className);
44+
} catch (Throwable $e) {
45+
// This locator asks every bootstrap-registered autoloader for the class, which is not
46+
// the order PHP uses: at runtime the class loader that resolves the class first is
47+
// often another one, so an autoloader that throws for names outside its own scope is
48+
// never invoked for them and its exception cannot happen. Letting it propagate here
49+
// aborts the analysis of the file with an internal error instead, so it is swallowed
50+
// and the remaining autoloaders and source locators get their turn. The class may
51+
// still have been defined before the throw, so the locators below run either way.
52+
// The autoloader ran for real, so its message is its real reason - with --debug,
53+
// the reporter prints it.
54+
// See https://github.com/phpstan/phpstan/issues/14976
55+
$this->exceptionReporter->report($className, $e);
56+
}
57+
4158
$reflection = $this->autoloadSourceLocator->locateIdentifier($reflector, $identifier);
4259
if ($reflection !== null) {
4360
return $reflection;

0 commit comments

Comments
 (0)