feature #28902 [Debug] Detect virtual methods using @method (ro0NL)
This PR was squashed before being merged into the 4.3-dev branch (closes #28902).
Discussion
----------
[Debug] Detect virtual methods using @method
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| BC breaks? | no <!-- see https://symfony.com/bc -->
| Deprecations? | no
| Tests pass? | yes <!-- please add some, will be required by reviewers -->
| Fixed tickets | https://github.com/symfony/symfony/pull/28897#issuecomment-430542792
| License | MIT
| Doc PR | https://github.com/symfony/symfony-docs/issues/10504
My first Debug PR, so im still on it. But early feedback welcome.
In #28901 we'll introduce a new virtual interface method using `@method` annotation. IIUC the idea is to trigger whenever such a method is overridden.
Commits
-------
38877c32ac
[Debug] Detect virtual methods using @method
This commit is contained in:
commit
5c2cee5c0f
@ -39,6 +39,7 @@ class DebugClassLoader
|
||||
private static $internalMethods = array();
|
||||
private static $annotatedParameters = array();
|
||||
private static $darwinCache = array('/' => array('/', array()));
|
||||
private static $method = array();
|
||||
|
||||
public function __construct(callable $classLoader)
|
||||
{
|
||||
@ -228,6 +229,24 @@ class DebugClassLoader
|
||||
self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($refl->isInterface() && false !== \strpos($doc, 'method') && preg_match_all('#\n \* @method\s+(static\s+)?+(?:[\w\|&\[\]\\\]+\s+)?(\w+(?:\s*\([^\)]*\))?)+(.+?([[:punct:]]\s*)?)?(?=\r?\n \*(?: @|/$|\r?\n))#', $doc, $notice, PREG_SET_ORDER)) {
|
||||
foreach ($notice as $method) {
|
||||
$static = '' !== $method[1];
|
||||
$name = $method[2];
|
||||
$description = $method[3] ?? null;
|
||||
if (false === strpos($name, '(')) {
|
||||
$name .= '()';
|
||||
}
|
||||
if (null !== $description) {
|
||||
$description = trim($description);
|
||||
if (!isset($method[4])) {
|
||||
$description .= '.';
|
||||
}
|
||||
}
|
||||
self::$method[$class][] = array($class, $name, $static, $description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$parent = \get_parent_class($class);
|
||||
@ -258,6 +277,28 @@ class DebugClassLoader
|
||||
if (isset(self::$internal[$use]) && \strncmp($ns, \str_replace('_', '\\', $use), $len)) {
|
||||
$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);
|
||||
}
|
||||
if (isset(self::$method[$use])) {
|
||||
if ($refl->isAbstract()) {
|
||||
if (isset(self::$method[$class])) {
|
||||
self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]);
|
||||
} else {
|
||||
self::$method[$class] = self::$method[$use];
|
||||
}
|
||||
} elseif (!$refl->isInterface()) {
|
||||
$hasCall = $refl->hasMethod('__call');
|
||||
$hasStaticCall = $refl->hasMethod('__callStatic');
|
||||
foreach (self::$method[$use] as $method) {
|
||||
list($interface, $name, $static, $description) = $method;
|
||||
if ($static ? $hasStaticCall : $hasCall) {
|
||||
continue;
|
||||
}
|
||||
$realName = substr($name, 0, strpos($name, '('));
|
||||
if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) {
|
||||
$deprecations[] = sprintf('Class "%s" should implement method "%s::%s"%s', $class, ($static ? 'static ' : '').$interface, $name, null == $description ? '.' : ': '.$description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (\trait_exists($class)) {
|
||||
|
@ -304,6 +304,46 @@ class DebugClassLoaderTest extends TestCase
|
||||
|
||||
$this->assertSame(array(), $deprecations);
|
||||
}
|
||||
|
||||
public function testVirtualUse()
|
||||
{
|
||||
$deprecations = array();
|
||||
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
|
||||
$e = error_reporting(E_USER_DEPRECATED);
|
||||
|
||||
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtual', true);
|
||||
|
||||
error_reporting($e);
|
||||
restore_error_handler();
|
||||
|
||||
$this->assertSame(array(
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::sameLineInterfaceMethodNoBraces()".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::newLineInterfaceMethod()": Some description!',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::newLineInterfaceMethodNoBraces()": Description.',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::invalidInterfaceMethod()".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::invalidInterfaceMethodNoBraces()".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::complexInterfaceMethod($arg, ...$args)".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::complexInterfaceMethodTyped($arg, int ...$args)": Description ...',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodNoBraces()".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodTyped(int $arg)": Description.',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodTypedNoBraces()".',
|
||||
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtual" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualSubInterface::subInterfaceMethod()".',
|
||||
), $deprecations);
|
||||
}
|
||||
|
||||
public function testVirtualUseWithMagicCall()
|
||||
{
|
||||
$deprecations = array();
|
||||
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
|
||||
$e = error_reporting(E_USER_DEPRECATED);
|
||||
|
||||
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtualMagicCall', true);
|
||||
|
||||
error_reporting($e);
|
||||
restore_error_handler();
|
||||
|
||||
$this->assertSame(array(), $deprecations);
|
||||
}
|
||||
}
|
||||
|
||||
class ClassLoader
|
||||
@ -359,6 +399,32 @@ class ClassLoader
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternalsParent extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { }');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\UseTraitWithInternalMethod' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; class UseTraitWithInternalMethod { use \\'.__NAMESPACE__.'\Fixtures\TraitWithInternalMethod; }');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtual' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtual extends ExtendsVirtualParent implements \\'.__NAMESPACE__.'\Fixtures\VirtualSubInterface {
|
||||
public function ownClassMethod() { }
|
||||
public function classMethod() { }
|
||||
public function sameLineInterfaceMethodNoBraces() { }
|
||||
}');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualParent' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualParent extends ExtendsVirtualAbstract {
|
||||
public function ownParentMethod() { }
|
||||
public function traitMethod() { }
|
||||
public function sameLineInterfaceMethod() { }
|
||||
public function staticMethodNoBraces() { } // should be static
|
||||
}');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstract' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstract extends ExtendsVirtualAbstractBase {
|
||||
public static function staticMethod() { }
|
||||
public function ownAbstractMethod() { }
|
||||
public function interfaceMethod() { }
|
||||
}');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstractBase' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstractBase extends \\'.__NAMESPACE__.'\Fixtures\VirtualClass implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface {
|
||||
public function ownAbstractBaseMethod() { }
|
||||
}');
|
||||
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualMagicCall' === $class) {
|
||||
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualMagicCall extends \\'.__NAMESPACE__.'\Fixtures\VirtualClassMagicCall implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface {
|
||||
}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
src/Symfony/Component/Debug/Tests/Fixtures/VirtualClass.php
Normal file
11
src/Symfony/Component/Debug/Tests/Fixtures/VirtualClass.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @method string classMethod()
|
||||
*/
|
||||
class VirtualClass
|
||||
{
|
||||
use VirtualTrait;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @method string magicMethod()
|
||||
* @method static string staticMagicMethod()
|
||||
*/
|
||||
class VirtualClassMagicCall
|
||||
{
|
||||
public static function __callStatic($name, $arguments)
|
||||
{
|
||||
}
|
||||
|
||||
public function __call($name, $arguments)
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @method string interfaceMethod()
|
||||
* @method sameLineInterfaceMethod($arg)
|
||||
* @method sameLineInterfaceMethodNoBraces
|
||||
*
|
||||
* Ignored
|
||||
* @method
|
||||
* @method
|
||||
*
|
||||
* Not ignored
|
||||
* @method newLineInterfaceMethod() Some description!
|
||||
* @method \stdClass newLineInterfaceMethodNoBraces Description
|
||||
*
|
||||
* Invalid
|
||||
* @method unknownType invalidInterfaceMethod()
|
||||
* @method unknownType|string invalidInterfaceMethodNoBraces
|
||||
*
|
||||
* Complex
|
||||
* @method complexInterfaceMethod($arg, ...$args)
|
||||
* @method string[]|int complexInterfaceMethodTyped($arg, int ...$args) Description ...
|
||||
*
|
||||
* Static
|
||||
* @method static Foo&Bar staticMethod()
|
||||
* @method static staticMethodNoBraces
|
||||
* @method static \stdClass staticMethodTyped(int $arg) Description
|
||||
* @method static \stdClass[] staticMethodTypedNoBraces
|
||||
*/
|
||||
interface VirtualInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @method string subInterfaceMethod()
|
||||
*/
|
||||
interface VirtualSubInterface extends VirtualInterface
|
||||
{
|
||||
}
|
10
src/Symfony/Component/Debug/Tests/Fixtures/VirtualTrait.php
Normal file
10
src/Symfony/Component/Debug/Tests/Fixtures/VirtualTrait.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||
|
||||
/**
|
||||
* @method string traitMethod()
|
||||
*/
|
||||
trait VirtualTrait
|
||||
{
|
||||
}
|
Reference in New Issue
Block a user