Skip to content

Commit

Permalink
feature #28902 [Debug] Detect virtual methods using @method (ro0NL)
Browse files Browse the repository at this point in the history
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 | #28897 (comment)
| License       | MIT
| Doc PR        | symfony/symfony-docs#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
-------

38877c3 [Debug] Detect virtual methods using @method
  • Loading branch information
fabpot committed Jan 5, 2019
2 parents 5053bfe + 38877c3 commit 5c2cee5
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 0 deletions.
41 changes: 41 additions & 0 deletions src/Symfony/Component/Debug/DebugClassLoader.php
Expand Up @@ -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)
{
Expand Down Expand Up @@ -228,6 +229,24 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
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);
Expand Down Expand Up @@ -258,6 +277,28 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
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)) {
Expand Down
66 changes: 66 additions & 0 deletions src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php
Expand Up @@ -304,6 +304,46 @@ class_exists('Test\\'.__NAMESPACE__.'\\UseTraitWithInternalMethod', true);

$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
Expand Down Expand Up @@ -359,6 +399,32 @@ public function internalMethod() { }
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 changes: 11 additions & 0 deletions src/Symfony/Component/Debug/Tests/Fixtures/VirtualClass.php
@@ -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)
{
}
}
34 changes: 34 additions & 0 deletions src/Symfony/Component/Debug/Tests/Fixtures/VirtualInterface.php
@@ -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
{
}
10 changes: 10 additions & 0 deletions src/Symfony/Component/Debug/Tests/Fixtures/VirtualSubInterface.php
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Debug\Tests\Fixtures;

/**
* @method string subInterfaceMethod()
*/
interface VirtualSubInterface extends VirtualInterface
{
}
10 changes: 10 additions & 0 deletions src/Symfony/Component/Debug/Tests/Fixtures/VirtualTrait.php
@@ -0,0 +1,10 @@
<?php

namespace Symfony\Component\Debug\Tests\Fixtures;

/**
* @method string traitMethod()
*/
trait VirtualTrait
{
}

0 comments on commit 5c2cee5

Please sign in to comment.