Skip to content

Commit 38877c3

Browse files
ro0NLfabpot
authored andcommitted
[Debug] Detect virtual methods using @method
1 parent 0acf9e1 commit 38877c3

File tree

7 files changed

+190
-0
lines changed

7 files changed

+190
-0
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class DebugClassLoader
3939
private static $internalMethods = array();
4040
private static $annotatedParameters = array();
4141
private static $darwinCache = array('/' => array('/', array()));
42+
private static $method = array();
4243

4344
public function __construct(callable $classLoader)
4445
{
@@ -228,6 +229,24 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
228229
self::${$annotation}[$class] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
229230
}
230231
}
232+
233+
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)) {
234+
foreach ($notice as $method) {
235+
$static = '' !== $method[1];
236+
$name = $method[2];
237+
$description = $method[3] ?? null;
238+
if (false === strpos($name, '(')) {
239+
$name .= '()';
240+
}
241+
if (null !== $description) {
242+
$description = trim($description);
243+
if (!isset($method[4])) {
244+
$description .= '.';
245+
}
246+
}
247+
self::$method[$class][] = array($class, $name, $static, $description);
248+
}
249+
}
231250
}
232251

233252
$parent = \get_parent_class($class);
@@ -258,6 +277,28 @@ public function checkAnnotations(\ReflectionClass $refl, $class)
258277
if (isset(self::$internal[$use]) && \strncmp($ns, $use, $len)) {
259278
$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);
260279
}
280+
if (isset(self::$method[$use])) {
281+
if ($refl->isAbstract()) {
282+
if (isset(self::$method[$class])) {
283+
self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]);
284+
} else {
285+
self::$method[$class] = self::$method[$use];
286+
}
287+
} elseif (!$refl->isInterface()) {
288+
$hasCall = $refl->hasMethod('__call');
289+
$hasStaticCall = $refl->hasMethod('__callStatic');
290+
foreach (self::$method[$use] as $method) {
291+
list($interface, $name, $static, $description) = $method;
292+
if ($static ? $hasStaticCall : $hasCall) {
293+
continue;
294+
}
295+
$realName = substr($name, 0, strpos($name, '('));
296+
if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || ($static && !$methodRefl->isStatic()) || (!$static && $methodRefl->isStatic())) {
297+
$deprecations[] = sprintf('Class "%s" should implement method "%s::%s"%s', $class, ($static ? 'static ' : '').$interface, $name, null == $description ? '.' : ': '.$description);
298+
}
299+
}
300+
}
301+
}
261302
}
262303

263304
if (\trait_exists($class)) {

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

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,46 @@ class_exists('Test\\'.__NAMESPACE__.'\\UseTraitWithInternalMethod', true);
304304

305305
$this->assertSame(array(), $deprecations);
306306
}
307+
308+
public function testVirtualUse()
309+
{
310+
$deprecations = array();
311+
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
312+
$e = error_reporting(E_USER_DEPRECATED);
313+
314+
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtual', true);
315+
316+
error_reporting($e);
317+
restore_error_handler();
318+
319+
$this->assertSame(array(
320+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::sameLineInterfaceMethodNoBraces()".',
321+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::newLineInterfaceMethod()": Some description!',
322+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::newLineInterfaceMethodNoBraces()": Description.',
323+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::invalidInterfaceMethod()".',
324+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::invalidInterfaceMethodNoBraces()".',
325+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::complexInterfaceMethod($arg, ...$args)".',
326+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::complexInterfaceMethodTyped($arg, int ...$args)": Description ...',
327+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodNoBraces()".',
328+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodTyped(int $arg)": Description.',
329+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtualParent" should implement method "static Symfony\Component\Debug\Tests\Fixtures\VirtualInterface::staticMethodTypedNoBraces()".',
330+
'Class "Test\Symfony\Component\Debug\Tests\ExtendsVirtual" should implement method "Symfony\Component\Debug\Tests\Fixtures\VirtualSubInterface::subInterfaceMethod()".',
331+
), $deprecations);
332+
}
333+
334+
public function testVirtualUseWithMagicCall()
335+
{
336+
$deprecations = array();
337+
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
338+
$e = error_reporting(E_USER_DEPRECATED);
339+
340+
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsVirtualMagicCall', true);
341+
342+
error_reporting($e);
343+
restore_error_handler();
344+
345+
$this->assertSame(array(), $deprecations);
346+
}
307347
}
308348

309349
class ClassLoader
@@ -359,6 +399,32 @@ public function internalMethod() { }
359399
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternalsParent extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface { }');
360400
} elseif ('Test\\'.__NAMESPACE__.'\UseTraitWithInternalMethod' === $class) {
361401
eval('namespace Test\\'.__NAMESPACE__.'; class UseTraitWithInternalMethod { use \\'.__NAMESPACE__.'\Fixtures\TraitWithInternalMethod; }');
402+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtual' === $class) {
403+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtual extends ExtendsVirtualParent implements \\'.__NAMESPACE__.'\Fixtures\VirtualSubInterface {
404+
public function ownClassMethod() { }
405+
public function classMethod() { }
406+
public function sameLineInterfaceMethodNoBraces() { }
407+
}');
408+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualParent' === $class) {
409+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualParent extends ExtendsVirtualAbstract {
410+
public function ownParentMethod() { }
411+
public function traitMethod() { }
412+
public function sameLineInterfaceMethod() { }
413+
public function staticMethodNoBraces() { } // should be static
414+
}');
415+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstract' === $class) {
416+
eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstract extends ExtendsVirtualAbstractBase {
417+
public static function staticMethod() { }
418+
public function ownAbstractMethod() { }
419+
public function interfaceMethod() { }
420+
}');
421+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualAbstractBase' === $class) {
422+
eval('namespace Test\\'.__NAMESPACE__.'; abstract class ExtendsVirtualAbstractBase extends \\'.__NAMESPACE__.'\Fixtures\VirtualClass implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface {
423+
public function ownAbstractBaseMethod() { }
424+
}');
425+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsVirtualMagicCall' === $class) {
426+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsVirtualMagicCall extends \\'.__NAMESPACE__.'\Fixtures\VirtualClassMagicCall implements \\'.__NAMESPACE__.'\Fixtures\VirtualInterface {
427+
}');
362428
}
363429
}
364430
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @method string classMethod()
7+
*/
8+
class VirtualClass
9+
{
10+
use VirtualTrait;
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @method string magicMethod()
7+
* @method static string staticMagicMethod()
8+
*/
9+
class VirtualClassMagicCall
10+
{
11+
public static function __callStatic($name, $arguments)
12+
{
13+
}
14+
15+
public function __call($name, $arguments)
16+
{
17+
}
18+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @method string interfaceMethod()
7+
* @method sameLineInterfaceMethod($arg)
8+
* @method sameLineInterfaceMethodNoBraces
9+
*
10+
* Ignored
11+
* @method
12+
* @method
13+
*
14+
* Not ignored
15+
* @method newLineInterfaceMethod() Some description!
16+
* @method \stdClass newLineInterfaceMethodNoBraces Description
17+
*
18+
* Invalid
19+
* @method unknownType invalidInterfaceMethod()
20+
* @method unknownType|string invalidInterfaceMethodNoBraces
21+
*
22+
* Complex
23+
* @method complexInterfaceMethod($arg, ...$args)
24+
* @method string[]|int complexInterfaceMethodTyped($arg, int ...$args) Description ...
25+
*
26+
* Static
27+
* @method static Foo&Bar staticMethod()
28+
* @method static staticMethodNoBraces
29+
* @method static \stdClass staticMethodTyped(int $arg) Description
30+
* @method static \stdClass[] staticMethodTypedNoBraces
31+
*/
32+
interface VirtualInterface
33+
{
34+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @method string subInterfaceMethod()
7+
*/
8+
interface VirtualSubInterface extends VirtualInterface
9+
{
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* @method string traitMethod()
7+
*/
8+
trait VirtualTrait
9+
{
10+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy