Skip to content

Commit 10fc13e

Browse files
fancywebnicolas-grekas
authored andcommitted
[ErrorHandler] Handle return types in DebugClassLoader
1 parent 507223d commit 10fc13e

File tree

8 files changed

+602
-12
lines changed

8 files changed

+602
-12
lines changed

src/Symfony/Component/ErrorHandler/DebugClassLoader.php

Lines changed: 271 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,43 @@
2727
*/
2828
class DebugClassLoader
2929
{
30+
private const SPECIAL_RETURN_TYPES = [
31+
'mixed' => 'mixed',
32+
'void' => 'void',
33+
'null' => 'null',
34+
'resource' => 'resource',
35+
'static' => 'object',
36+
'$this' => 'object',
37+
'boolean' => 'bool',
38+
'true' => 'bool',
39+
'false' => 'bool',
40+
'integer' => 'int',
41+
'array' => 'array',
42+
'bool' => 'bool',
43+
'callable' => 'callable',
44+
'float' => 'float',
45+
'int' => 'integer',
46+
'iterable' => 'iterable',
47+
'object' => 'object',
48+
'string' => 'string',
49+
'self' => 'self',
50+
'parent' => 'parent',
51+
];
52+
53+
private const BUILTIN_RETURN_TYPES = [
54+
'void' => true,
55+
'array' => true,
56+
'bool' => true,
57+
'callable' => true,
58+
'float' => true,
59+
'int' => true,
60+
'iterable' => true,
61+
'object' => true,
62+
'string' => true,
63+
'self' => true,
64+
'parent' => true,
65+
];
66+
3067
private $classLoader;
3168
private $isFinder;
3269
private $loaded = [];
@@ -40,6 +77,8 @@ class DebugClassLoader
4077
private static $annotatedParameters = [];
4178
private static $darwinCache = ['/' => ['/', []]];
4279
private static $method = [];
80+
private static $returnTypes = [];
81+
private static $methodTraits = [];
4382

4483
public function __construct(callable $classLoader)
4584
{
@@ -218,11 +257,11 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
218257
$deprecations = [];
219258

220259
// Don't trigger deprecations for classes in the same vendor
221-
if (2 > $len = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) {
222-
$len = 0;
223-
$ns = '';
260+
if (2 > $vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_'))) {
261+
$vendorLen = 0;
262+
$vendor = '';
224263
} else {
225-
$ns = str_replace('_', '\\', substr($class, 0, $len));
264+
$vendor = str_replace('_', '\\', substr($class, 0, $vendorLen));
226265
}
227266

228267
// Detect annotations on the class
@@ -252,7 +291,7 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
252291
}
253292
}
254293

255-
$parent = get_parent_class($class);
294+
$parent = get_parent_class($class) ?: null;
256295
$parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent);
257296
if ($parent) {
258297
$parentAndOwnInterfaces[$parent] = $parent;
@@ -271,13 +310,13 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
271310
if (!isset(self::$checkedClasses[$use])) {
272311
$this->checkClass($use);
273312
}
274-
if (isset(self::$deprecated[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len) && !isset(self::$deprecated[$class])) {
313+
if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class])) {
275314
$type = class_exists($class, false) ? 'class' : (interface_exists($class, false) ? 'interface' : 'trait');
276315
$verb = class_exists($use, false) || interface_exists($class, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
277316

278317
$deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s.', $class, $type, $verb, $use, self::$deprecated[$use]);
279318
}
280-
if (isset(self::$internal[$use]) && strncmp($ns, str_replace('_', '\\', $use), $len)) {
319+
if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) {
281320
$deprecations[] = sprintf('The "%s" %s is considered internal%s. It may change without further notice. You should not use it from "%s".', $use, class_exists($use, false) ? 'class' : (interface_exists($use, false) ? 'interface' : 'trait'), self::$internal[$use], $class);
282321
}
283322
if (isset(self::$method[$use])) {
@@ -305,15 +344,24 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
305344
}
306345

307346
if (trait_exists($class)) {
347+
$file = $refl->getFileName();
348+
349+
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
350+
if ($method->getFileName() === $file) {
351+
self::$methodTraits[$file][$method->getStartLine()] = $class;
352+
}
353+
}
354+
308355
return $deprecations;
309356
}
310357

311-
// Inherit @final, @internal and @param annotations for methods
358+
// Inherit @final, @internal, @param and @return annotations for methods
312359
self::$finalMethods[$class] = [];
313360
self::$internalMethods[$class] = [];
314361
self::$annotatedParameters[$class] = [];
362+
self::$returnTypes[$class] = [];
315363
foreach ($parentAndOwnInterfaces as $use) {
316-
foreach (['finalMethods', 'internalMethods', 'annotatedParameters'] as $property) {
364+
foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes'] as $property) {
317365
if (isset(self::${$property}[$use])) {
318366
self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use];
319367
}
@@ -325,6 +373,16 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
325373
continue;
326374
}
327375

376+
if (null === $ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null) {
377+
$ns = $vendor;
378+
$len = $vendorLen;
379+
} elseif (2 > $len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_'))) {
380+
$len = 0;
381+
$ns = '';
382+
} else {
383+
$ns = str_replace('_', '\\', substr($ns, 0, $len));
384+
}
385+
328386
if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
329387
list($declaringClass, $message) = self::$finalMethods[$parent][$method->name];
330388
$deprecations[] = sprintf('The "%s::%s()" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $declaringClass, $method->name, $message, $class);
@@ -353,10 +411,26 @@ public function checkAnnotations(\ReflectionClass $refl, string $class): array
353411
}
354412
}
355413

414+
if (isset(self::$returnTypes[$class][$method->name]) && !$method->hasReturnType() && !($doc && preg_match('/\n\s+\* @return +(\S+)/', $doc))) {
415+
list($returnType, $declaringClass, $declaringFile) = self::$returnTypes[$class][$method->name];
416+
417+
if (strncmp($ns, $declaringClass, $len)) {
418+
//if (0 === strpos($class, 'Symfony\\')) {
419+
// self::patchMethod($method, $returnType, $declaringFile);
420+
//}
421+
422+
$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, $returnType, $class);
423+
}
424+
}
425+
356426
if (!$doc) {
357427
continue;
358428
}
359429

430+
if (!$method->hasReturnType() && false !== strpos($doc, '@return') && preg_match('/\n\s+\* @return +(\S+)/', $doc, $matches)) {
431+
$this->setReturnType($matches[1], $method, $parent);
432+
}
433+
360434
$finalOrInternal = false;
361435

362436
foreach (['final', 'internal'] as $annotation) {
@@ -496,11 +570,9 @@ private function darwinRealpath(string $real): string
496570
/**
497571
* `class_implements` includes interfaces from the parents so we have to manually exclude them.
498572
*
499-
* @param string|false $parent
500-
*
501573
* @return string[]
502574
*/
503-
private function getOwnInterfaces(string $class, $parent): array
575+
private function getOwnInterfaces(string $class, ?string $parent): array
504576
{
505577
$ownInterfaces = class_implements($class, false);
506578

@@ -518,4 +590,191 @@ private function getOwnInterfaces(string $class, $parent): array
518590

519591
return $ownInterfaces;
520592
}
593+
594+
private function setReturnType(string $types, \ReflectionMethod $method, ?string $parent): void
595+
{
596+
$nullable = false;
597+
$typesMap = [];
598+
foreach (explode('|', $types) as $t) {
599+
$t = $this->normalizeType($t, $method->class, $parent);
600+
$typesMap[strtolower($t)] = $t;
601+
}
602+
603+
if (isset($typesMap['array']) && isset($typesMap['iterable'])) {
604+
if ('[]' === substr($typesMap['array'], -2)) {
605+
$typesMap['iterable'] = $typesMap['array'];
606+
}
607+
unset($typesMap['array']);
608+
}
609+
610+
$normalizedType = key($typesMap);
611+
$returnType = current($typesMap);
612+
613+
foreach ($typesMap as $n => $t) {
614+
if ('null' === $n) {
615+
$nullable = true;
616+
} elseif ('null' === $normalizedType) {
617+
$normalizedType = $t;
618+
$returnType = $t;
619+
} elseif ($n !== $normalizedType) {
620+
// ignore multi-types return declarations
621+
return;
622+
}
623+
}
624+
625+
if ('void' === $normalizedType) {
626+
$nullable = false;
627+
} elseif (!isset(self::BUILTIN_RETURN_TYPES[$normalizedType]) && isset(self::SPECIAL_RETURN_TYPES[$normalizedType])) {
628+
// ignore other special return types
629+
return;
630+
}
631+
632+
if ($nullable) {
633+
$returnType = '?'.$returnType;
634+
}
635+
636+
self::$returnTypes[$method->class][$method->name] = [$returnType, $method->class, $method->getFileName()];
637+
}
638+
639+
private function normalizeType(string $type, string $class, ?string $parent): string
640+
{
641+
if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) {
642+
if ('parent' === $lcType = self::SPECIAL_RETURN_TYPES[$lcType]) {
643+
$lcType = null !== $parent ? '\\'.$parent : 'parent';
644+
} elseif ('self' === $lcType) {
645+
$lcType = '\\'.$class;
646+
}
647+
648+
return $lcType;
649+
}
650+
651+
if ('[]' === substr($type, -2)) {
652+
return 'array';
653+
}
654+
655+
if (preg_match('/^(array|iterable|callable) *[<(]/', $lcType, $m)) {
656+
return $m[1];
657+
}
658+
659+
// We could resolve "use" statements to return the FQDN
660+
// but this would be too expensive for a runtime checker
661+
662+
return $type;
663+
}
664+
665+
/**
666+
* Utility method to add @return annotations to the Symfony code-base where it triggers a self-deprecations.
667+
*/
668+
private static function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile)
669+
{
670+
static $patchedMethods = [];
671+
static $useStatements = [];
672+
673+
if (!file_exists($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) {
674+
return;
675+
}
676+
677+
$patchedMethods[$file][$startLine] = true;
678+
$patchedMethods[$file][0] = $patchedMethods[$file][0] ?? 0;
679+
$startLine += $patchedMethods[$file][0] - 2;
680+
$nullable = '?' === $returnType[0] ? '?' : '';
681+
$returnType = ltrim($returnType, '?');
682+
$code = file($file);
683+
684+
if (!isset(self::BUILTIN_RETURN_TYPES[$returnType]) && ('\\' !== $returnType[0] || $p = strrpos($returnType, '\\', 1))) {
685+
list($namespace, $useOffset, $useMap) = $useStatements[$file] ?? $useStatements[$file] = self::getUseStatements($file);
686+
687+
if ('\\' !== $returnType[0]) {
688+
list($declaringNamespace, , $declaringUseMap) = $useStatements[$declaringFile] ?? $useStatements[$declaringFile] = self::getUseStatements($declaringFile);
689+
690+
$p = strpos($returnType, '\\', 1);
691+
$alias = $p ? substr($returnType, 0, $p) : $returnType;
692+
693+
if (isset($declaringUseMap[$alias])) {
694+
$returnType = '\\'.$declaringUseMap[$alias].($p ? substr($returnType, $p) : '');
695+
} else {
696+
$returnType = '\\'.$declaringNamespace.$returnType;
697+
}
698+
699+
$p = strrpos($returnType, '\\', 1);
700+
}
701+
702+
$alias = substr($returnType, 1 + $p);
703+
$returnType = substr($returnType, 1);
704+
705+
if (!isset($useMap[$alias]) && (class_exists($c = $namespace.$alias) || interface_exists($c) || trait_exists($c))) {
706+
$useMap[$alias] = $c;
707+
}
708+
709+
if (!isset($useMap[$alias])) {
710+
$useStatements[$file][2][$alias] = $returnType;
711+
$code[$useOffset] = "use $returnType;\n".$code[$useOffset];
712+
++$patchedMethods[$file][0];
713+
} elseif ($useMap[$alias] !== $returnType) {
714+
$alias .= 'FIXME';
715+
$useStatements[$file][2][$alias] = $returnType;
716+
$code[$useOffset] = "use $returnType as $alias;\n".$code[$useOffset];
717+
++$patchedMethods[$file][0];
718+
}
719+
720+
$returnType = $alias;
721+
}
722+
723+
if ($method->getDocComment()) {
724+
$code[$startLine] = " * @return $nullable$returnType\n".$code[$startLine];
725+
} else {
726+
$code[$startLine] .= <<<EOTXT
727+
/**
728+
* @return $nullable$returnType
729+
*/
730+
731+
EOTXT;
732+
}
733+
734+
$patchedMethods[$file][0] += substr_count($code[$startLine], "\n") - 1;
735+
file_put_contents($file, $code);
736+
}
737+
738+
private static function getUseStatements(string $file): array
739+
{
740+
$namespace = '';
741+
$useMap = [];
742+
$useOffset = 0;
743+
744+
if (!file_exists($file)) {
745+
return [$namespace, $useOffset, $useMap];
746+
}
747+
748+
$file = file($file);
749+
750+
for ($i = 0; $i < \count($file); ++$i) {
751+
if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) {
752+
break;
753+
}
754+
755+
if (0 === strpos($file[$i], 'namespace ')) {
756+
$namespace = substr($file[$i], \strlen('namespace '), -2).'\\';
757+
$useOffset = $i + 2;
758+
}
759+
760+
if (0 === strpos($file[$i], 'use ')) {
761+
$useOffset = $i;
762+
763+
for (; 0 === strpos($file[$i], 'use '); ++$i) {
764+
$u = explode(' as ', substr($file[$i], 4, -2), 2);
765+
766+
if (1 === \count($u)) {
767+
$p = strrpos($u[0], '\\');
768+
$useMap[substr($u[0], false !== $p ? 1 + $p : 0)] = $u[0];
769+
} else {
770+
$useMap[$u[1]] = $u[0];
771+
}
772+
}
773+
774+
break;
775+
}
776+
}
777+
778+
return [$namespace, $useOffset, $useMap];
779+
}
521780
}

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