Skip to content

Commit 47aa5a7

Browse files
[ErrorHandler] improve DebugClassLoader patching logic
1 parent 153e081 commit 47aa5a7

File tree

1 file changed

+71
-26
lines changed

1 file changed

+71
-26
lines changed

src/Symfony/Component/ErrorHandler/DebugClassLoader.php

+71-26
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class DebugClassLoader
6767
private $classLoader;
6868
private $isFinder;
6969
private $loaded = [];
70-
private $compatPatch;
70+
private $patchTypes;
7171
private static $caseCheck;
7272
private static $checkedClasses = [];
7373
private static $final = [];
@@ -85,7 +85,7 @@ public function __construct(callable $classLoader)
8585
{
8686
$this->classLoader = $classLoader;
8787
$this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
88-
$this->compatPatch = getenv('SYMFONY_PATCH_TYPE_DECLARATIONS_COMPAT') ?: null;
88+
parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);
8989

9090
if (!isset(self::$caseCheck)) {
9191
$file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
@@ -160,13 +160,22 @@ public static function disable(): void
160160
spl_autoload_unregister($function);
161161
}
162162

163+
$loader = null;
164+
163165
foreach ($functions as $function) {
164166
if (\is_array($function) && $function[0] instanceof self) {
167+
$loader = $function[0];
165168
$function = $function[0]->getClassLoader();
166169
}
167170

168171
spl_autoload_register($function);
169172
}
173+
174+
if (null !== $loader) {
175+
foreach (array_merge(get_declared_interfaces(), get_declared_traits(), get_declared_classes()) as $class) {
176+
$loader->checkClass($class);
177+
}
178+
}
170179
}
171180

172181
public function findFile(string $class): ?string
@@ -348,7 +357,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
348357
if (trait_exists($class)) {
349358
$file = $refl->getFileName();
350359

351-
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
360+
foreach ($refl->getMethods() as $method) {
352361
if ($method->getFileName() === $file) {
353362
self::$methodTraits[$file][$method->getStartLine()] = $class;
354363
}
@@ -370,7 +379,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
370379
}
371380
}
372381

373-
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
382+
foreach ($refl->getMethods() as $method) {
374383
if ($method->class !== $class) {
375384
continue;
376385
}
@@ -413,18 +422,24 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
413422
}
414423
}
415424

425+
if ($canAddReturnType = '' !== ($this->patchTypes['class-prefix'] ?? '') && 0 === strpos($class, $this->patchTypes['class-prefix'])) {
426+
$canAddReturnType = false !== strpos($refl->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR)
427+
|| $refl->isFinal()
428+
|| $method->isFinal()
429+
|| $method->isPrivate()
430+
|| ('' === (self::$internal[$class] ?? null) && !$refl->isAbstract())
431+
|| '' === (self::$final[$class] ?? null)
432+
|| preg_match('/@(final|internal)$/m', $doc);
433+
}
434+
416435
if (isset(self::$returnTypes[$class][$method->name]) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
417436
list($normalizedType, $returnType, $declaringClass, $declaringFile) = self::$returnTypes[$class][$method->name];
418437

419-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
420-
self::fixReturnStatements($method, $normalizedType);
438+
if ($canAddReturnType) {
439+
$this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
421440
}
422441

423442
if (strncmp($ns, $declaringClass, $len)) {
424-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
425-
self::patchMethod($method, $returnType, $declaringFile);
426-
}
427-
428443
$deprecations[] = sprintf('Method "%s::%s()" will return "%s" as of its next major version. Doing the same in child class "%s" will be required when upgrading.', $declaringClass, $method->name, $normalizedType, $class);
429444
}
430445
}
@@ -436,11 +451,19 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
436451
if (!$method->hasReturnType() && false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) {
437452
$this->setReturnType($matches[1], $method, $parent);
438453

439-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
440-
self::fixReturnStatements($method, self::$returnTypes[$class][$method->name][0] ?? '?');
454+
if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
455+
$this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
456+
}
457+
458+
if ($method->isPrivate()) {
459+
unset(self::$returnTypes[$class][$method->name]);
441460
}
442461
}
443462

463+
if ($method->isPrivate()) {
464+
continue;
465+
}
466+
444467
$finalOrInternal = false;
445468

446469
foreach (['final', 'internal'] as $annotation) {
@@ -630,7 +653,7 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
630653
} elseif ('null' === $normalizedType) {
631654
$normalizedType = $t;
632655
$returnType = $t;
633-
} elseif ($n !== $normalizedType) {
656+
} elseif ($n !== $normalizedType || !preg_match('/^\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+)*+$/', $n)) {
634657
// ignore multi-types return declarations
635658
return;
636659
}
@@ -680,7 +703,7 @@ private function normalizeType(string $type, string $class, ?string $parent): st
680703
/**
681704
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
682705
*/
683-
private static function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile)
706+
private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
684707
{
685708
static $patchedMethods = [];
686709
static $useStatements = [];
@@ -690,8 +713,10 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
690713
}
691714

692715
$patchedMethods[$file][$startLine] = true;
693-
$patchedMethods[$file][0] = $patchedMethods[$file][0] ?? 0;
694-
$startLine += $patchedMethods[$file][0] - 2;
716+
$fileOffset = $patchedMethods[$file][0] ?? 0;
717+
$startLine += $fileOffset - 2;
718+
$nullable = '?' === $normalizedType[0] ? '?' : '';
719+
$normalizedType = ltrim($normalizedType, '?');
695720
$returnType = explode('|', $returnType);
696721
$code = file($file);
697722

@@ -737,32 +762,42 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
737762
if (!isset($useMap[$alias])) {
738763
$useStatements[$file][2][$alias] = $type;
739764
$code[$useOffset] = "use $type;\n".$code[$useOffset];
740-
++$patchedMethods[$file][0];
765+
++$fileOffset;
741766
} elseif ($useMap[$alias] !== $type) {
742767
$alias .= 'FIXME';
743768
$useStatements[$file][2][$alias] = $type;
744769
$code[$useOffset] = "use $type as $alias;\n".$code[$useOffset];
745-
++$patchedMethods[$file][0];
770+
++$fileOffset;
746771
}
747772

748773
$returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;
774+
775+
if (!isset(self::SPECIAL_RETURN_TYPES[$normalizedType]) && !isset(self::SPECIAL_RETURN_TYPES[$returnType[$i]])) {
776+
$normalizedType = $returnType[$i];
777+
}
749778
}
750779

751-
$returnType = implode('|', $returnType);
780+
if ('object' === $normalizedType && ($this->patchTypes['keep-compat-with-php71'] ?? false)) {
781+
$returnType = implode('|', $returnType);
752782

753-
if ($method->getDocComment()) {
754-
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
755-
} else {
756-
$code[$startLine] .= <<<EOTXT
783+
if ($method->getDocComment()) {
784+
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
785+
} else {
786+
$code[$startLine] .= <<<EOTXT
757787
/**
758788
* @return $returnType
759789
*/
760790
761791
EOTXT;
792+
}
793+
794+
$fileOffset += substr_count($code[$startLine], "\n") - 1;
762795
}
763796

764-
$patchedMethods[$file][0] += substr_count($code[$startLine], "\n") - 1;
797+
$patchedMethods[$file][0] = $fileOffset;
765798
file_put_contents($file, $code);
799+
800+
$this->fixReturnStatements($method, $nullable.$normalizedType, $patchedMethods[$file][0]);
766801
}
767802

768803
private static function getUseStatements(string $file): array
@@ -808,15 +843,25 @@ private static function getUseStatements(string $file): array
808843
return [$namespace, $useOffset, $useMap];
809844
}
810845

811-
private static function fixReturnStatements(\ReflectionMethod $method, string $returnType)
846+
private function fixReturnStatements(\ReflectionMethod $method, string $returnType, int $i = 0)
812847
{
848+
if (($this->patchTypes['keep-compat-with-php71'] ?? false) && 'object' === ltrim($returnType, '?')) {
849+
return;
850+
}
851+
813852
if (!file_exists($file = $method->getFileName())) {
814853
return;
815854
}
816855

817856
$fixedCode = $code = file($file);
857+
$i += $method->getStartLine();
858+
859+
if ('?' !== $returnType) {
860+
$fixedCode[$i - 1] = preg_replace('/\)(;?\n)/', "): $returnType\\1", $code[$i - 1]);
861+
}
862+
818863
$end = $method->getEndLine();
819-
for ($i = $method->getStartLine(); $i < $end; ++$i) {
864+
for (; $i < $end; ++$i) {
820865
if ('void' === $returnType) {
821866
$fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]);
822867
} elseif ('mixed' === $returnType || '?' === $returnType[0]) {

0 commit comments

Comments
 (0)