Skip to content

Commit ecec254

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

File tree

1 file changed

+177
-31
lines changed

1 file changed

+177
-31
lines changed

src/Symfony/Component/ErrorHandler/DebugClassLoader.php

Lines changed: 177 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ class DebugClassLoader
4242
'bool' => 'bool',
4343
'callable' => 'callable',
4444
'float' => 'float',
45-
'int' => 'integer',
45+
'int' => 'int',
4646
'iterable' => 'iterable',
4747
'object' => 'object',
4848
'string' => 'string',
@@ -64,10 +64,79 @@ class DebugClassLoader
6464
'parent' => true,
6565
];
6666

67+
private const MAGIC_METHODS = [
68+
'__set' => 'void',
69+
'__isset' => 'bool',
70+
'__unset' => 'void',
71+
'__sleep' => 'array',
72+
'__wakeup' => 'void',
73+
'__toString' => 'string',
74+
'__clone' => 'void',
75+
'__debugInfo' => 'array',
76+
'__serialize' => 'array',
77+
'__unserialize' => 'void',
78+
];
79+
80+
private const INTERNAL_TYPES = [
81+
'ArrayAccess' => [
82+
'offsetExists' => 'bool',
83+
'offsetSet' => 'void',
84+
'offsetUnset' => 'void',
85+
],
86+
'Countable' => [
87+
'count' => 'int',
88+
],
89+
'Iterator' => [
90+
'next' => 'void',
91+
'valid' => 'bool',
92+
'rewind' => 'void',
93+
],
94+
'IteratorAggregate' => [
95+
'getIterator' => '\Traversable',
96+
],
97+
'OuterIterator' => [
98+
'getInnerIterator' => '\Iterator',
99+
],
100+
'RecursiveIterator' => [
101+
'hasChildren' => 'bool',
102+
],
103+
'SeekableIterator' => [
104+
'seek' => 'void',
105+
],
106+
'Serializable' => [
107+
'serialize' => 'string',
108+
'unserialize' => 'void',
109+
],
110+
'SessionHandlerInterface' => [
111+
'open' => 'bool',
112+
'close' => 'bool',
113+
'read' => 'string',
114+
'write' => 'bool',
115+
'destroy' => 'bool',
116+
'gc' => 'bool',
117+
],
118+
'SessionIdInterface' => [
119+
'create_sid' => 'string',
120+
],
121+
'SessionUpdateTimestampHandlerInterface' => [
122+
'validateId' => 'bool',
123+
'updateTimestamp' => 'bool',
124+
],
125+
'Throwable' => [
126+
'getMessage' => 'string',
127+
'getCode' => 'int',
128+
'getFile' => 'string',
129+
'getLine' => 'int',
130+
'getTrace' => 'array',
131+
'getPrevious' => '?\Throwable',
132+
'getTraceAsString' => 'string',
133+
],
134+
];
135+
67136
private $classLoader;
68137
private $isFinder;
69138
private $loaded = [];
70-
private $compatPatch;
139+
private $patchTypes;
71140
private static $caseCheck;
72141
private static $checkedClasses = [];
73142
private static $final = [];
@@ -80,12 +149,13 @@ class DebugClassLoader
80149
private static $method = [];
81150
private static $returnTypes = [];
82151
private static $methodTraits = [];
152+
private static $fileOffsets = [];
83153

84154
public function __construct(callable $classLoader)
85155
{
86156
$this->classLoader = $classLoader;
87157
$this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile');
88-
$this->compatPatch = getenv('SYMFONY_PATCH_TYPE_DECLARATIONS_COMPAT') ?: null;
158+
parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes);
89159

90160
if (!isset(self::$caseCheck)) {
91161
$file = file_exists(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR);
@@ -160,13 +230,22 @@ public static function disable(): void
160230
spl_autoload_unregister($function);
161231
}
162232

233+
$loader = null;
234+
163235
foreach ($functions as $function) {
164236
if (\is_array($function) && $function[0] instanceof self) {
237+
$loader = $function[0];
165238
$function = $function[0]->getClassLoader();
166239
}
167240

168241
spl_autoload_register($function);
169242
}
243+
244+
if (null !== $loader) {
245+
foreach (array_merge(get_declared_interfaces(), get_declared_traits(), get_declared_classes()) as $class) {
246+
$loader->checkClass($class);
247+
}
248+
}
170249
}
171250

172251
public function findFile(string $class): ?string
@@ -348,7 +427,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
348427
if (trait_exists($class)) {
349428
$file = $refl->getFileName();
350429

351-
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
430+
foreach ($refl->getMethods() as $method) {
352431
if ($method->getFileName() === $file) {
353432
self::$methodTraits[$file][$method->getStartLine()] = $class;
354433
}
@@ -368,9 +447,17 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
368447
self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
369448
}
370449
}
450+
451+
if (null !== (self::INTERNAL_TYPES[$use] ?? null)) {
452+
foreach (self::INTERNAL_TYPES[$use] as $method => $returnType) {
453+
if ('void' !== $returnType) {
454+
self::$returnTypes[$class] += [$method => [$returnType, $returnType, $class, '']];
455+
}
456+
}
457+
}
371458
}
372459

373-
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
460+
foreach ($refl->getMethods() as $method) {
374461
if ($method->class !== $class) {
375462
continue;
376463
}
@@ -413,34 +500,71 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
413500
}
414501
}
415502

416-
if (isset(self::$returnTypes[$class][$method->name]) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
417-
list($normalizedType, $returnType, $declaringClass, $declaringFile) = self::$returnTypes[$class][$method->name];
503+
$forcePatchTypes = $this->patchTypes['force'] ?? null;
504+
505+
if ($canAddReturnType = null !== $forcePatchTypes && false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) {
506+
if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) {
507+
$this->patchTypes['force'] = $forcePatchTypes ?: 'docblock';
508+
}
509+
510+
$canAddReturnType = ($this->patchTypes['force'] ?? false)
511+
|| false !== strpos($refl->getFileName(), \DIRECTORY_SEPARATOR.'Tests'.\DIRECTORY_SEPARATOR)
512+
|| $refl->isFinal()
513+
|| $method->isFinal()
514+
|| $method->isPrivate()
515+
|| ('' === (self::$internal[$class] ?? null) && !$refl->isAbstract())
516+
|| '' === (self::$final[$class] ?? null)
517+
|| preg_match('/@(final|internal)$/m', $doc)
518+
;
519+
}
520+
521+
if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? self::MAGIC_METHODS[$method->name] ?? null) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
522+
if ('void' === $returnType) {
523+
$canAddReturnType = false;
524+
}
525+
526+
list($normalizedType, $returnType, $declaringClass, $declaringFile) = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType;
418527

419-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
420-
self::fixReturnStatements($method, $normalizedType);
528+
if ($canAddReturnType && 'docblock' !== ($this->patchTypes['force'] ?? false)) {
529+
$this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
421530
}
422531

423532
if (strncmp($ns, $declaringClass, $len)) {
424-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
425-
self::patchMethod($method, $returnType, $declaringFile);
533+
if ('docblock' === ($this->patchTypes['force'] ?? false) && false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR)) {
534+
$this->patchMethod($method, $returnType, $declaringFile, $normalizedType);
535+
} elseif ('' !== $declaringClass) {
536+
$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);
426537
}
427-
428-
$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);
429538
}
430539
}
431540

432541
if (!$doc) {
542+
$this->patchTypes['force'] = $forcePatchTypes;
543+
433544
continue;
434545
}
435546

436-
if (!$method->hasReturnType() && false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) {
547+
$matches = [];
548+
549+
if (!$method->hasReturnType() && ((false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void'))) {
550+
$matches = $matches ?: [1 => self::MAGIC_METHODS[$method->name]];
437551
$this->setReturnType($matches[1], $method, $parent);
438552

439-
if (null !== $this->compatPatch && 0 === strpos($class, $this->compatPatch)) {
440-
self::fixReturnStatements($method, self::$returnTypes[$class][$method->name][0] ?? '?');
553+
if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) {
554+
$this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]);
555+
}
556+
557+
if ($method->isPrivate()) {
558+
unset(self::$returnTypes[$class][$method->name]);
441559
}
442560
}
443561

562+
$this->patchTypes['force'] = $forcePatchTypes;
563+
564+
if ($method->isPrivate()) {
565+
continue;
566+
}
567+
444568
$finalOrInternal = false;
445569

446570
foreach (['final', 'internal'] as $annotation) {
@@ -630,7 +754,7 @@ private function setReturnType(string $types, \ReflectionMethod $method, ?string
630754
} elseif ('null' === $normalizedType) {
631755
$normalizedType = $t;
632756
$returnType = $t;
633-
} elseif ($n !== $normalizedType) {
757+
} 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)) {
634758
// ignore multi-types return declarations
635759
return;
636760
}
@@ -680,7 +804,7 @@ private function normalizeType(string $type, string $class, ?string $parent): st
680804
/**
681805
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
682806
*/
683-
private static function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile)
807+
private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType)
684808
{
685809
static $patchedMethods = [];
686810
static $useStatements = [];
@@ -690,8 +814,10 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
690814
}
691815

692816
$patchedMethods[$file][$startLine] = true;
693-
$patchedMethods[$file][0] = $patchedMethods[$file][0] ?? 0;
694-
$startLine += $patchedMethods[$file][0] - 2;
817+
$fileOffset = self::$fileOffsets[$file] ?? 0;
818+
$startLine += $fileOffset - 2;
819+
$nullable = '?' === $normalizedType[0] ? '?' : '';
820+
$normalizedType = ltrim($normalizedType, '?');
695821
$returnType = explode('|', $returnType);
696822
$code = file($file);
697823

@@ -737,32 +863,42 @@ private static function patchMethod(\ReflectionMethod $method, string $returnTyp
737863
if (!isset($useMap[$alias])) {
738864
$useStatements[$file][2][$alias] = $type;
739865
$code[$useOffset] = "use $type;\n".$code[$useOffset];
740-
++$patchedMethods[$file][0];
866+
++$fileOffset;
741867
} elseif ($useMap[$alias] !== $type) {
742868
$alias .= 'FIXME';
743869
$useStatements[$file][2][$alias] = $type;
744870
$code[$useOffset] = "use $type as $alias;\n".$code[$useOffset];
745-
++$patchedMethods[$file][0];
871+
++$fileOffset;
746872
}
747873

748874
$returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias;
875+
876+
if (!isset(self::SPECIAL_RETURN_TYPES[$normalizedType]) && !isset(self::SPECIAL_RETURN_TYPES[$returnType[$i]])) {
877+
$normalizedType = $returnType[$i];
878+
}
749879
}
750880

751-
$returnType = implode('|', $returnType);
881+
if ('docblock' === ($this->patchTypes['force'] ?? null) || ('object' === $normalizedType && ($this->patchTypes['php71-compat'] ?? false))) {
882+
$returnType = implode('|', $returnType);
752883

753-
if ($method->getDocComment()) {
754-
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
755-
} else {
756-
$code[$startLine] .= <<<EOTXT
884+
if ($method->getDocComment()) {
885+
$code[$startLine] = " * @return $returnType\n".$code[$startLine];
886+
} else {
887+
$code[$startLine] .= <<<EOTXT
757888
/**
758889
* @return $returnType
759890
*/
760891
761892
EOTXT;
893+
}
894+
895+
$fileOffset += substr_count($code[$startLine], "\n") - 1;
762896
}
763897

764-
$patchedMethods[$file][0] += substr_count($code[$startLine], "\n") - 1;
898+
self::$fileOffsets[$file] = $fileOffset;
765899
file_put_contents($file, $code);
900+
901+
$this->fixReturnStatements($method, $nullable.$normalizedType);
766902
}
767903

768904
private static function getUseStatements(string $file): array
@@ -808,15 +944,25 @@ private static function getUseStatements(string $file): array
808944
return [$namespace, $useOffset, $useMap];
809945
}
810946

811-
private static function fixReturnStatements(\ReflectionMethod $method, string $returnType)
947+
private function fixReturnStatements(\ReflectionMethod $method, string $returnType)
812948
{
949+
if (($this->patchTypes['php71-compat'] ?? false) && 'object' === ltrim($returnType, '?') && 'docblock' !== ($this->patchTypes['force'] ?? null)) {
950+
return;
951+
}
952+
813953
if (!file_exists($file = $method->getFileName())) {
814954
return;
815955
}
816956

817957
$fixedCode = $code = file($file);
818-
$end = $method->getEndLine();
819-
for ($i = $method->getStartLine(); $i < $end; ++$i) {
958+
$i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine();
959+
960+
if ('?' !== $returnType && 'docblock' !== ($this->patchTypes['force'] ?? null)) {
961+
$fixedCode[$i - 1] = preg_replace('/\)(;?\n)/', "): $returnType\\1", $code[$i - 1]);
962+
}
963+
964+
$end = $method->isGenerator() ? $i : $method->getEndLine();
965+
for (; $i < $end; ++$i) {
820966
if ('void' === $returnType) {
821967
$fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]);
822968
} elseif ('mixed' === $returnType || '?' === $returnType[0]) {

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy