Skip to content

Commit 924141c

Browse files
committed
[Debug][DebugClassLoader] Handle return types
1 parent f8664e7 commit 924141c

File tree

7 files changed

+1124
-0
lines changed

7 files changed

+1124
-0
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@
2727
*/
2828
class DebugClassLoader
2929
{
30+
private const COMMON_NON_OBJECT_RETURNED_TYPES = [
31+
'false',
32+
'mixed',
33+
'null',
34+
'resource',
35+
'static',
36+
'true',
37+
'$this',
38+
];
39+
40+
private const NON_NULLABLE_RETURNABLE_TYPES = [
41+
'void' => '"void"',
42+
];
43+
44+
private const NULLABLE_RETURNABLE_TYPES = [
45+
'array' => 'an array',
46+
'bool' => 'a boolean',
47+
'callable' => 'a callable',
48+
'float' => 'a float',
49+
'int' => 'an integer',
50+
'iterable' => 'an iterable',
51+
'object' => 'an object',
52+
'string' => 'a string',
53+
'self' => 'an instance of itself',
54+
];
55+
3056
private $classLoader;
3157
private $isFinder;
3258
private $loaded = [];
@@ -40,6 +66,7 @@ class DebugClassLoader
4066
private static $annotatedParameters = [];
4167
private static $darwinCache = ['/' => ['/', []]];
4268
private static $method = [];
69+
private static $returnTypes = [];
4370

4471
public function __construct(callable $classLoader)
4572
{
@@ -309,6 +336,8 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
309336
}
310337
}
311338

339+
$this->checkReturnTypes($class, $parent, $refl, $ns, $len, $deprecations);
340+
312341
if (\trait_exists($class)) {
313342
return $deprecations;
314343
}
@@ -372,6 +401,8 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
372401
}
373402
}
374403

404+
$this->saveReturnType($class, $method, $doc);
405+
375406
if ($finalOrInternal || $method->isConstructor() || false === \strpos($doc, '@param') || StatelessInvocation::class === $class) {
376407
continue;
377408
}
@@ -522,4 +553,151 @@ private function getOwnInterfaces($class, $parent)
522553

523554
return $ownInterfaces;
524555
}
556+
557+
private function saveReturnType(string $class, \ReflectionMethod $method, string $doc): void
558+
{
559+
if ($method->getReturnType() instanceof \ReflectionType) {
560+
return;
561+
}
562+
563+
if (false === \strpos($doc, 'return')) {
564+
return;
565+
}
566+
567+
if (!preg_match_all('/\n\s+\* @return +(\S+)/', $doc, $matches, PREG_SET_ORDER)) {
568+
return;
569+
}
570+
571+
$types = explode('|', end($matches)[1], 3);
572+
573+
$nullable = false;
574+
575+
switch (\count($types)) {
576+
case 1:
577+
$type = $types[0];
578+
579+
break;
580+
case 2:
581+
// fastest way to handle the null|null edge case
582+
$type = 'null';
583+
584+
foreach (array_unique(array_map([$this, 'normalizeType'], $types)) as $i => $normalizedType) {
585+
if ('null' === $normalizedType) {
586+
$nullable = true;
587+
} else {
588+
$type = $types[$i];
589+
}
590+
}
591+
592+
if (!$nullable) {
593+
return;
594+
}
595+
596+
break;
597+
default:
598+
return;
599+
}
600+
601+
$normalizedType = $this->normalizeType($type);
602+
if (\in_array($normalizedType, self::COMMON_NON_OBJECT_RETURNED_TYPES)) {
603+
return;
604+
}
605+
606+
if (isset(self::NULLABLE_RETURNABLE_TYPES[$normalizedType])) {
607+
$normalizedDocReturnType = self::NULLABLE_RETURNABLE_TYPES[$normalizedType];
608+
$hintedReturnType = $normalizedType;
609+
} elseif (isset(self::NON_NULLABLE_RETURNABLE_TYPES[$normalizedType])) {
610+
if ($nullable) {
611+
return;
612+
}
613+
614+
$normalizedDocReturnType = self::NON_NULLABLE_RETURNABLE_TYPES[$normalizedType];
615+
$hintedReturnType = $normalizedType;
616+
} else {
617+
$normalizedDocReturnType = sprintf('an instance of "%s"', $type);
618+
$hintedReturnType = $type;
619+
}
620+
621+
if ($nullable) {
622+
$normalizedDocReturnType .= ' or null';
623+
$hintedReturnType = '?'.$hintedReturnType;
624+
}
625+
626+
self::$returnTypes[$class][$method->name] = [$normalizedDocReturnType, $hintedReturnType];
627+
}
628+
629+
/**
630+
* @param string $class
631+
* @param string|false $parent
632+
* @param \ReflectionClass $refl
633+
* @param string $ns
634+
* @param int $len
635+
* @param array $deprecations
636+
*/
637+
private function checkReturnTypes(string $class, $parent, \ReflectionClass $refl, string $ns, int $len, &$deprecations): void
638+
{
639+
$parentsAndInterfacesThatHaveMethodsThatReturnsADocType = [];
640+
foreach (array_merge($parent ? array_merge([$parent], class_parents($parent, false)) : [], class_implements($class, false)) as $use) {
641+
if (isset(self::$returnTypes[$use]) && 0 !== \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
642+
$parentsAndInterfacesThatHaveMethodsThatReturnsADocType[] = $use;
643+
}
644+
}
645+
646+
if (empty($parentsAndInterfacesThatHaveMethodsThatReturnsADocType)) {
647+
return;
648+
}
649+
650+
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
651+
if ($method->class !== $class || $method->getReturnType() instanceof \ReflectionType) {
652+
continue;
653+
}
654+
655+
foreach ($parentsAndInterfacesThatHaveMethodsThatReturnsADocType as $name) {
656+
if (!isset(self::$returnTypes[$name][$method->name])) {
657+
continue;
658+
}
659+
660+
list($normalizedDocReturnType, $hintedReturnType) = self::$returnTypes[$name][$method->name];
661+
$deprecations[] = sprintf(
662+
'The "%s::%s()" parent method returns %s. You could safely add the ": %s" return type to your method to be compatible with the next major version of its parent.',
663+
$class,
664+
$method->name,
665+
$normalizedDocReturnType,
666+
$hintedReturnType
667+
);
668+
669+
break;
670+
}
671+
}
672+
}
673+
674+
private function normalizeType(string $type): string
675+
{
676+
$lcType = mb_strtolower($type);
677+
678+
if (
679+
isset(self::NULLABLE_RETURNABLE_TYPES[$lcType]) ||
680+
isset(self::NON_NULLABLE_RETURNABLE_TYPES[$lcType]) ||
681+
isset(self::COMMON_NON_OBJECT_RETURNED_TYPES[$lcType])) {
682+
return $lcType;
683+
}
684+
685+
if ('[]' === substr($lcType, -2)) {
686+
return 'array';
687+
}
688+
689+
if ('integer' === $lcType) {
690+
return 'int';
691+
}
692+
693+
if ('boolean' === $lcType) {
694+
return 'bool';
695+
}
696+
697+
if (1 === preg_match('/^callable *\(/', $lcType)) {
698+
return 'callable';
699+
}
700+
701+
return $lcType;
702+
}
525703
}

0 commit comments

Comments
 (0)