22
33namespace PHPStan \Reflection \BetterReflection \SourceLocator ;
44
5+ use Composer \Autoload \ClassLoader ;
56use Override ;
67use ParseError ;
78use PhpParser \Node \Arg ;
3132use function defined ;
3233use function function_exists ;
3334use function interface_exists ;
35+ use function is_array ;
3436use function is_file ;
3537use function is_string ;
3638use function opcache_invalidate ;
39+ use function realpath ;
3740use function restore_error_handler ;
3841use function set_error_handler ;
3942use function spl_autoload_functions ;
43+ use function str_contains ;
4044use function strtolower ;
4145use function trait_exists ;
4246use const PHP_VERSION_ID ;
@@ -336,14 +340,21 @@ private function locateClassByName(string $className): ?array
336340 $ this ->silenceErrors ();
337341
338342 try {
343+ $ foundByComposer = false ;
339344 $ result = FileReadTrapStreamWrapper ::withStreamWrapperOverride (
340- static function () use ($ className ): ?array {
345+ static function () use ($ className, & $ foundByComposer ): ?array {
341346 $ functions = spl_autoload_functions ();
342347 if ($ functions === false ) {
343348 return null ;
344349 }
345350
346351 foreach ($ functions as $ preExistingAutoloader ) {
352+ $ composerFile = self ::findFileWithComposer ($ preExistingAutoloader , $ className );
353+ if ($ composerFile !== null ) {
354+ $ foundByComposer = true ;
355+ return [[$ composerFile ], $ className , null ];
356+ }
357+
347358 try {
348359 $ preExistingAutoloader ($ className );
349360 } catch (ParseError ) {
@@ -370,7 +381,8 @@ static function () use ($className): ?array {
370381 return null ;
371382 }
372383
373- if (!function_exists ('opcache_invalidate ' )) {
384+ // nothing was included, so there is no trapped script to invalidate
385+ if ($ foundByComposer || !function_exists ('opcache_invalidate ' )) {
374386 return $ result ;
375387 }
376388
@@ -387,6 +399,39 @@ static function () use ($className): ?array {
387399 }
388400 }
389401
402+ /**
403+ * Asks Composer's ClassLoader which file loadClass() would include, without
404+ * including it. With OPcache holding a file this process already ran, an
405+ * include inside the trap runs the cached script instead of the trap's empty
406+ * one, and a file declaring a function dies with "Cannot redeclare".
407+ *
408+ * A path behind a stream wrapper is left to the trap: realpath() cannot
409+ * resolve it, and the phar's own ClassLoader maps PHPStan\ to phar://.
410+ *
411+ * @param callable(string): void $autoloader
412+ */
413+ private static function findFileWithComposer (callable $ autoloader , string $ className ): ?string
414+ {
415+ if (!is_array ($ autoloader ) || !$ autoloader [0 ] instanceof ClassLoader) {
416+ return null ;
417+ }
418+
419+ $ file = $ autoloader [0 ]->findFile ($ className );
420+ if ($ file === false || str_contains ($ file , ':// ' )) {
421+ return null ;
422+ }
423+
424+ // findFile() can return ../ segments and mixed separators; resolve them
425+ // as an include would, so that locateIdentifier() can compare the path
426+ // with ReflectionClass::getFileName()
427+ $ resolvedFile = realpath ($ file );
428+ if ($ resolvedFile === false || !is_file ($ resolvedFile )) {
429+ return null ;
430+ }
431+
432+ return $ resolvedFile ;
433+ }
434+
390435 private function silenceErrors (): void
391436 {
392437 set_error_handler (static fn (): bool => true );
0 commit comments