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:
Fabien Potencier 2019-01-05 09:11:12 +01:00
commit 5c2cee5c0f
7 changed files with 190 additions and 0 deletions

View File

@ -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)) {

View File

@ -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 {
}');
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
/**
* @method string classMethod()
*/
class VirtualClass
{
use VirtualTrait;
}

View File

@ -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)
{
}
}

View File

@ -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
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
/**
* @method string subInterfaceMethod()
*/
interface VirtualSubInterface extends VirtualInterface
{
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
/**
* @method string traitMethod()
*/
trait VirtualTrait
{
}