-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Debug] Trigger a deprecation for new parameters not defined in sub classes #28329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,6 +11,8 @@ | |
|
||
namespace Symfony\Component\Debug; | ||
|
||
use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; | ||
|
||
/** | ||
* Autoloader checking if the class is really defined in the file found. | ||
* | ||
|
@@ -21,6 +23,7 @@ | |
* @author Fabien Potencier <fabien@symfony.com> | ||
* @author Christophe Coevoet <stof@notk.org> | ||
* @author Nicolas Grekas <p@tchwork.com> | ||
* @author Guilhem Niot <guilhem.niot@gmail.com> | ||
*/ | ||
class DebugClassLoader | ||
{ | ||
|
@@ -34,6 +37,7 @@ class DebugClassLoader | |
private static $deprecated = array(); | ||
private static $internal = array(); | ||
private static $internalMethods = array(); | ||
private static $annotatedParameters = array(); | ||
private static $darwinCache = array('/' => array('/', array())); | ||
|
||
public function __construct(callable $classLoader) | ||
|
@@ -200,9 +204,12 @@ private function checkClass($class, $file = null) | |
} | ||
} | ||
8000 |
|
|
$parent = \get_parent_class($class); | ||
$parentAndTraits = \class_uses($name, false); | ||
if ($parent = \get_parent_class($class)) { | ||
$parentAndTraits[] = $parent; | ||
$parentAndOwnInterfaces = $this->getOwnInterfaces($name, $parent); | ||
if ($parent) { | ||
$parentAndTraits[$parent] = $parent; | ||
$parentAndOwnInterfaces[$parent] = $parent; | ||
|
||
if (!isset(self::$checkedClasses[$parent])) { | ||
$this->checkClass($parent); | ||
|
@@ -214,7 +221,7 @@ private function checkClass($class, $file = null) | |
} | ||
|
||
// Detect if the parent is annotated | ||
foreach ($parentAndTraits + $this->getOwnInterfaces($name, $parent) as $use) { | ||
foreach ($parentAndTraits + $parentAndOwnInterfaces as $use) { | ||
if (!isset(self::$checkedClasses[$use])) { | ||
$this->checkClass($use); | ||
} | ||
|
@@ -229,11 +236,17 @@ private function checkClass($class, $file = null) | |
} | ||
} | ||
|
||
// Inherit @final and @internal annotations for methods | ||
// Inherit @final, @internal and @param annotations for methods | ||
self::$finalMethods[$name] = array(); | ||
self::$internalMethods[$name] = array(); | ||
foreach ($parentAndTraits as $use) { | ||
foreach (array('finalMethods', 'internalMethods') as $property) { | ||
self::$annotatedParameters[$name] = array(); | ||
$map = array( | ||
'finalMethods' => $parentAndTraits, | ||
'internalMethods' => $parentAndTraits, | ||
'annotatedParameters' => $parentAndOwnInterfaces, // We don't parse traits params | ||
); | ||
foreach ($map as $property => $uses) { | ||
foreach ($uses as $use) { | ||
if (isset(self::${$property}[$use])) { | ||
self::${$property}[$name] = self::${$property}[$name] ? self::${$property}[$use] + self::${$property}[$name] : self::${$property}[$use]; | ||
} | ||
|
@@ -258,20 +271,56 @@ private function checkClass($class, $file = null) | |
} | ||
} | ||
|
||
// Method from a trait | ||
if ($method->getFilename() !== $refl->getFileName()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be kept imo, I think we also don't want methods from traits being checked against their own There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. worth a new test case :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍, you were right about this change.
Thinking of it again, I prefer not parsing |
||
// To read method annotations | ||
$doc = $method->getDocComment(); | ||
|
||
if (isset(self::$annotatedParameters[$name][$method->name])) { | ||
$definedParameters = array(); | ||
foreach ($method->getParameters() as $parameter) { | ||
$definedParameters[$parameter->name] = true; | ||
} | ||
|
||
foreach (self::$annotatedParameters[$name][$method->name] as $parameterName => $deprecation) { | ||
if (!isset($definedParameters[$parameterName]) && !($doc && preg_match("/\\n\\s+\\* @param (.*?)(?<= )\\\${$parameterName}\\b/", $doc))) { | ||
@trigger_error(sprintf($deprecation, $name), E_USER_DEPRECATED); | ||
} | ||
} | ||
} | ||
|
||
if (!$doc) { | ||
continue; | ||
} | ||
|
||
// Detect method annotations | ||
if (false === $doc = $method->getDocComment()) { | ||
$finalOrInternal = false; | ||
|
||
// Skip methods from traits | ||
if ($method->getFilename() === $refl->getFileName()) { | ||
foreach (array('final', 'internal') as $annotation) { | ||
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { | ||
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; | ||
self::${$annotation.'Methods'}[$name][$method->name] = array($name, $message); | ||
$finalOrInternal = true; | ||
} | ||
} | ||
} | ||
|
||
if ($finalOrInternal || $method->isConstructor() || false === \strpos($doc, '@param') || StatelessInvocation::class === $name) { | ||
continue; | ||
} | ||
|
||
foreach (array('final', 'internal') as $annotation) { | ||
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { | ||
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; | ||
self::${$annotation.'Methods'}[$name][$method->name] = array($name, $message); | ||
if (!preg_match_all('#\n\s+\* @param (.*?)(?<= )\$([a-zA-Z0-9_\x7f-\xff]++)#', $doc, $matches, PREG_SET_ORDER)) { | ||
continue; | ||
} | ||
if (!isset(self::$annotatedParameters[$name][$method->name])) { | ||
$definedParameters = array(); | ||
foreach ($method->getParameters() as $parameter) { | ||
$definedParameters[$parameter->name] = true; | ||
} | ||
} | ||
foreach ($matches as list(, $parameterType, $parameterName)) { | ||
if (!isset($definedParameters[$parameterName])) { | ||
$parameterType = trim($parameterType); | ||
self::$annotatedParameters[$name][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its parent class "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType.' ' : '', $parameterName, $method->class); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Debug\Tests\Fixtures; | ||
|
||
class ClassWithAnnotatedParameters | ||
{ | ||
/** | ||
* @param string $foo this is a foo parameter | ||
*/ | ||
public function fooMethod(string $foo) | ||
{ | ||
} | ||
|
||
/** | ||
* @param string $bar parameter not implemented yet | ||
*/ | ||
public function barMethod(/* string $bar = null */) | ||
{ | ||
} | ||
|
||
/** | ||
* @param Quz $quz parameter not implemented yet | ||
*/ | ||
public function quzMethod(/* Quz $quz = null */) | ||
{ | ||
} | ||
|
||
/** | ||
* @param true $yes | ||
*/ | ||
public function isSymfony() | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Debug\Tests\Fixtures; | ||
|
||
/** | ||
* Ensures a deprecation is triggered when a new parameter is not declared in child classes. | ||
*/ | ||
interface InterfaceWithAnnotatedParameters | ||
{ | ||
/** | ||
* @param bool $matrix | ||
*/ | ||
public function whereAmI(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Debug\Tests\Fixtures; | ||
|
||
class SubClassWithAnnotatedParameters extends ClassWithAnnotatedParameters implements InterfaceWithAnnotatedParameters | ||
{ | ||
use TraitWithAnnotatedParameters; | ||
|
||
public function fooMethod(string $foo) | ||
{ | ||
} | ||
|
||
public function barMethod($bar = null) | ||
{ | ||
} | ||
|
||
public function quzMethod() | ||
{ | ||
} | ||
|
||
public function whereAmI() | ||
{ | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<?php | ||
|
||
namespace Symfony\Component\Debug\Tests\Fixtures; | ||
|
||
trait TraitWithAnnotatedParameters | ||
{ | ||
/** | ||
* `@param` annotations in traits are not parsed. | ||
*/ | ||
public function isSymfony() | ||
{ | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related to sebastianbergmann/phpunit-mock-objects#427