|
2 | 2 |
|
3 | 3 | namespace PHPStan\Reflection\BetterReflection\SourceLocator; |
4 | 4 |
|
| 5 | +use Composer\Autoload\ClassLoader; |
5 | 6 | use Override; |
6 | 7 | use ParseError; |
7 | 8 | use PhpParser\Node\Arg; |
|
31 | 32 | use function defined; |
32 | 33 | use function function_exists; |
33 | 34 | use function interface_exists; |
| 35 | +use function is_array; |
34 | 36 | use function is_file; |
35 | 37 | use function is_string; |
36 | 38 | use function opcache_invalidate; |
| 39 | +use function realpath; |
37 | 40 | use function restore_error_handler; |
38 | 41 | use function set_error_handler; |
39 | 42 | use function spl_autoload_functions; |
@@ -333,58 +336,99 @@ private function locateClassByName(string $className): ?array |
333 | 336 | return null; |
334 | 337 | } |
335 | 338 |
|
| 339 | + $autoloadFunctions = spl_autoload_functions(); |
| 340 | + if ($autoloadFunctions === false) { |
| 341 | + return null; |
| 342 | + } |
| 343 | + |
336 | 344 | $this->silenceErrors(); |
337 | 345 |
|
338 | 346 | try { |
339 | | - $result = FileReadTrapStreamWrapper::withStreamWrapperOverride( |
340 | | - static function () use ($className): ?array { |
341 | | - $functions = spl_autoload_functions(); |
342 | | - if ($functions === false) { |
343 | | - return null; |
344 | | - } |
| 347 | + return self::locateThroughAutoloaders($autoloadFunctions, $className); |
| 348 | + } finally { |
| 349 | + restore_error_handler(); |
| 350 | + } |
| 351 | + } |
345 | 352 |
|
346 | | - foreach ($functions as $preExistingAutoloader) { |
347 | | - try { |
348 | | - $preExistingAutoloader($className); |
349 | | - } catch (ParseError) { |
350 | | - // the trap served a parse error instead of the empty |
351 | | - // script, see FileReadTrapStreamWrapper::stream_read(); |
352 | | - // the file was recorded before the include compiled it |
353 | | - } |
| 353 | + /** |
| 354 | + * Runs the registered autoloaders, in order, to find which file declares a |
| 355 | + * class - asking Composer's where it would look instead of letting it get |
| 356 | + * there by including the file. |
| 357 | + * |
| 358 | + * ClassLoader::findFile() answers with the same path loadClass() would |
| 359 | + * include, without running anything. Nothing is compiled, so the file cannot |
| 360 | + * execute a second time - which it otherwise does whenever OPcache already |
| 361 | + * holds the script: the include is then served from the cache without the |
| 362 | + * trap being asked for the contents at all, and a file declaring a function |
| 363 | + * fatals with "Cannot redeclare". That is the function-per-file layout of |
| 364 | + * php-standard-library and azjezz/psl, whose files-autoload bootstrap has |
| 365 | + * already loaded every path their PSR-4 prefix also resolves to. |
| 366 | + * |
| 367 | + * Every other autoloader still runs inside the trap, one at a time so that |
| 368 | + * registration order is preserved either way: an autoloader ahead of |
| 369 | + * Composer's may well claim a name Composer would resolve elsewhere. |
| 370 | + * |
| 371 | + * @param list<callable(string): void> $autoloadFunctions |
| 372 | + * @return array{string[], string, int|null}|null |
| 373 | + */ |
| 374 | + private static function locateThroughAutoloaders(array $autoloadFunctions, string $className): ?array |
| 375 | + { |
| 376 | + foreach ($autoloadFunctions as $autoloadFunction) { |
| 377 | + if (is_array($autoloadFunction) && $autoloadFunction[0] instanceof ClassLoader) { |
| 378 | + $file = $autoloadFunction[0]->findFile($className); |
| 379 | + if ($file === false) { |
| 380 | + continue; |
| 381 | + } |
354 | 382 |
|
355 | | - /** |
356 | | - * This static variable is populated by the side-effect of the stream wrapper |
357 | | - * trying to read the file path when `include()` is used by an autoloader. |
358 | | - * |
359 | | - * This will not be `null` when the autoloader tried to read a file. |
360 | | - */ |
361 | | - if (FileReadTrapStreamWrapper::$autoloadLocatedFiles !== []) { |
362 | | - return [FileReadTrapStreamWrapper::$autoloadLocatedFiles, $className, null]; |
363 | | - } |
| 383 | + // findFile() concatenates the mapped prefix with the rest of the |
| 384 | + // name, so its answer can carry ../ segments and mixed |
| 385 | + // separators. PHP resolves an include path before the trap ever |
| 386 | + // sees it, and locateIdentifier() matches what lands here against |
| 387 | + // ReflectionClass::getFileName(), which is resolved too. |
| 388 | + $resolvedFile = realpath($file); |
| 389 | + |
| 390 | + // a class map can outlive the file it points at, and loadClass() |
| 391 | + // would move on to the next autoloader just the same |
| 392 | + if ($resolvedFile === false || !is_file($resolvedFile)) { |
| 393 | + continue; |
| 394 | + } |
| 395 | + |
| 396 | + return [[$resolvedFile], $className, null]; |
| 397 | + } |
| 398 | + |
| 399 | + $locatedFiles = FileReadTrapStreamWrapper::withStreamWrapperOverride( |
| 400 | + static function () use ($autoloadFunction, $className): array { |
| 401 | + try { |
| 402 | + $autoloadFunction($className); |
| 403 | + } catch (ParseError) { |
| 404 | + // the trap served a parse error instead of the empty |
| 405 | + // script, see FileReadTrapStreamWrapper::stream_read(); |
| 406 | + // the file was recorded before the include compiled it |
364 | 407 | } |
365 | 408 |
|
366 | | - return null; |
| 409 | + // populated by the side effect of the stream wrapper being |
| 410 | + // asked for the file an include() reached for |
| 411 | + return FileReadTrapStreamWrapper::$autoloadLocatedFiles; |
367 | 412 | }, |
368 | 413 | ); |
369 | | - if ($result === null) { |
370 | | - return null; |
371 | | - } |
372 | 414 |
|
373 | | - if (!function_exists('opcache_invalidate')) { |
374 | | - return $result; |
| 415 | + if ($locatedFiles === []) { |
| 416 | + continue; |
375 | 417 | } |
376 | 418 |
|
377 | 419 | // the trap's empty script got compiled - and cached, with OPcache |
378 | 420 | // active. Where this call cannot reach the entry, the trap served a |
379 | 421 | // parse error instead, see FileReadTrapStreamWrapper::stream_read() |
380 | | - foreach ($result[0] as $file) { |
381 | | - opcache_invalidate($file, true); |
| 422 | + if (function_exists('opcache_invalidate')) { |
| 423 | + foreach ($locatedFiles as $locatedFile) { |
| 424 | + opcache_invalidate($locatedFile, true); |
| 425 | + } |
382 | 426 | } |
383 | 427 |
|
384 | | - return $result; |
385 | | - } finally { |
386 | | - restore_error_handler(); |
| 428 | + return [$locatedFiles, $className, null]; |
387 | 429 | } |
| 430 | + |
| 431 | + return null; |
388 | 432 | } |
389 | 433 |
|
390 | 434 | private function silenceErrors(): void |
|
0 commit comments