Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8e781fe

Browse files
committed
[Debug][DebugClassLoader] Handle return types
1 parent cf8cfeb commit 8e781fe

8 files changed

+485
-1
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 180 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,10 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
309336
}
310337
}
311338

339+
if (PHP_VERSION_ID >= 70100) {
340+
$this->checkReturnTypes($class, $parent, $refl, $deprecations);
341+
}
342+
312343
if (trait_exists($class)) {
313344
return $deprecations;
314345
}
@@ -372,6 +403,10 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
372403
}
373404
}
374405

406+
if (PHP_VERSION_ID >= 70100 && 0 === strpos($class, 'Symfony\\')) {
407+
$this->saveReturnType($class, $method, $doc);
408+
}
409+
375410
if ($finalOrInternal || $method->isConstructor() || false === strpos($doc, '@param') || StatelessInvocation::class === $class) {
376411
continue;
377412
}
@@ -522,4 +557,149 @@ private function getOwnInterfaces($class, $parent)
522557

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

src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Debug\DebugClassLoader;
16+
use Test\Symfony\Component\Debug\Tests\Fixtures\OutsideInterface;
1617

1718
class DebugClassLoaderTest extends TestCase
1819
{
@@ -361,6 +362,35 @@ public function testEvaluatedCode()
361362
{
362363
$this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\DefinitionInEvaluatedCode', true));
363364
}
365+
366+
public function testReturnType(): void
367+
{
368+
$deprecations = [];
369+
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
370+
$e = error_reporting(E_USER_DEPRECATED);
371+
372+
class_exists('Test\\'.__NAMESPACE__.'\\ReturnType', true);
373+
374+
error_reporting($e);
375+
restore_error_handler();
376+
377+
$this->assertSame(PHP_VERSION_ID >= 70100 ? [
378+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::returnTypeGrandParent()" parent method returns a string. You could safely add the ": string" return type to your method to be compatible with the next major version of its parent.',
379+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::returnTypeGrandParentInterface()" parent method returns a string. You could safely add the ": string" return type to your method to be compatible with the next major version of its parent.',
380+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::returnTypeInterface()" parent method returns a string. You could safely add the ": string" return type to your method to be compatible with the next major version of its parent.',
381+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::oneNonNullableReturnableType()" parent method returns "void". You could safely add the ": void" return type to your method to be compatible with the next major version of its parent.',
382+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::oneNullableReturnableType()" parent method returns an array. You could safely add the ": array" return type to your method to be compatible with the next major version of its parent.',
383+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::oneNullableReturnableTypeWithNull()" parent method returns a boolean or null. You could safely add the ": ?bool" return type to your method to be compatible with the next major version of its parent.',
384+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::oneOtherType()" parent method returns an instance of "\ArrayIterator". You could safely add the ": \ArrayIterator" return type to your method to be compatible with the next major version of its parent.',
385+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::oneOtherTypeWithNull()" parent method returns an instance of "\ArrayIterator" or null. You could safely add the ": ?\ArrayIterator" return type to your method to be compatible with the next major version of its parent.',
386+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::nullableReturnableTypeNormalization()" parent method returns an object. You could safely add the ": object" return type to your method to be compatible with the next major version of its parent.',
387+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::nonNullableReturnableTypeNormalization()" parent method returns "void". You could safely add the ": void" return type to your method to be compatible with the next major version of its parent.',
388+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::bracketsNormalization()" parent method returns an array. You could safely add the ": array" return type to your method to be compatible with the next major version of its parent.',
389+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::callableNormalization1()" parent method returns a callable. You could safely add the ": callable" return type to your method to be compatible with the next major version of its parent.',
390+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::callableNormalization2()" parent method returns a callable. You could safely add the ": callable" return type to your method to be compatible with the next major version of its parent.',
391+
'The "Test\Symfony\Component\Debug\Tests\ReturnType::otherTypeNormalization()" parent method returns an instance of "\ArrayIterator". You could safely add the ": \ArrayIterator" return type to your method to be compatible with the next major version of its parent.',
392+
] : [], $deprecations);
393+
}
364394
}
365395

366396
class ClassLoader
@@ -443,6 +473,36 @@ public function ownAbstractBaseMethod() { }
443473
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualMagicCall' === $class) {
444474
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualMagicCall extends \\'.__NAMESPACE__.'\Fixtures\VirtualClassMagicCall implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface {
445475
}');
476+
} elseif ('Test\\'.__NAMESPACE__.'\ReturnType' === $class) {
477+
eval('namespace Test\\'.__NAMESPACE__.'; class ReturnType extends \\'.__NAMESPACE__.'\Fixtures\ReturnTypeParent implements \\'.__NAMESPACE__.'\Fixtures\ReturnTypeInterface, \\'.__NAMESPACE__.'\Fixtures\ReturnTypeInterface, \\Test\\'.__NAMESPACE__.'\\Fixtures\\OutsideInterface {
478+
public function returnTypeGrandParent() { }
479+
public function returnTypeGrandParentInterface() { }
480+
public function returnTypeInterface() { }
481+
public function realReturnTypeMustBeThere(): string { }
482+
public function realReturnTypeIsAlreadyThere(): float { }
483+
public function realReturnTypeIsAlreadyThereWithNull(): ?iterable { }
484+
public function oneCommonNonObjectReturnedType() { }
485+
public function oneCommonNonObjectReturnedTypeWithNull() { }
486+
public function oneNonNullableReturnableType() { }
487+
public function oneNonNullableReturnableTypeWithNull() { }
488+
public function oneNullableReturnableType() { }
489+
public function oneNullableReturnableTypeWithNull() { }
490+
public function oneOtherType() { }
491+
public function oneOtherTypeWithNull() { }
492+
public function twoNullableReturnableTypes() { }
493+
public function twoNullEdgeCase() { }
494+
public function threeReturnTypes() { }
495+
public function nullableReturnableTypeNormalization() { }
496+
public function nonNullableReturnableTypeNormalization() { }
497+
public function commonNonObjectReturnedTypeNormalization() { }
498+
public function bracketsNormalization() { }
499+
public function callableNormalization1() { }
500+
public function callableNormalization2() { }
501+
public function otherTypeNormalization() { }
502+
public function outsideMethod() { }
503+
}');
504+
} elseif ('Test\\'.__NAMESPACE__.'\Fixtures\OutsideInterface' === $class) {
505+
return $fixtureDir.\DIRECTORY_SEPARATOR.'OutsideInterface.php';
446506
}
447507
}
448508
}

src/Symfony/Component/Debug/Tests/Fixtures/FinalMethod2Trait.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
trait FinalMethod2Trait
66
{
7-
public function finalMethod2()
7+
public function finalMethod2(): int
88
{
99
}
1010
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Test\Symfony\Component\Debug\Tests\Fixtures;
4+
5+
interface OutsideInterface
6+
{
7+
/**
8+
* @return void
9+
*/
10+
public function outsideMethod();
11+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
abstract class ReturnTypeGrandParent implements ReturnTypeGrandParentInterface
6+
{
7+
/**
8+
* @return string
9+
*/
10+
public function returnTypeGrandParent()
11+
{
12+
// expected => string
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
interface ReturnTypeGrandParentInterface
6+
{
7+
// expected => string
8+
9+
/**
10+
* @return string
11+
*/
12+
public function returnTypeGrandParentInterface();
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
interface ReturnTypeInterface
6+
{
7+
// expected => string
8+
9+
/**
10+
* @return string
11+
*/
12+
public function returnTypeInterface();
13+
}

0 commit comments

Comments
 (0)