[Debug] Trigger a deprecation for new parameters not defined in sub classes

This commit is contained in:
Guilhem Niot 2018-08-31 16:35:24 +02:00 committed by Nicolas Grekas
parent 0d9154e5bb
commit 1f5d8b62f7
8 changed files with 174 additions and 14 deletions

View File

@ -23,6 +23,8 @@ class Logger extends BaseLogger implements DebugLoggerInterface, ResetInterface
{
/**
* {@inheritdoc}
*
* @param Request|null $request
*/
public function getLogs(/* Request $request = null */)
{
@ -39,6 +41,8 @@ class Logger extends BaseLogger implements DebugLoggerInterface, ResetInterface
/**
* {@inheritdoc}
*
* @param Request|null $request
*/
public function countErrors(/* Request $request = null */)
{

View File

@ -58,6 +58,8 @@ class DebugProcessor implements DebugLoggerInterface, ResetInterface
/**
* {@inheritdoc}
*
* @param Request|null $request
*/
public function getLogs(/* Request $request = null */)
{
@ -78,6 +80,8 @@ class DebugProcessor implements DebugLoggerInterface, ResetInterface
/**
* {@inheritdoc}
*
* @param Request|null $request
*/
public function countErrors(/* Request $request = null */)
{

View File

@ -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 @@ namespace Symfony\Component\Debug;
* @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 @@ class DebugClassLoader
}
}
$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 @@ class DebugClassLoader
}
// 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 @@ class DebugClassLoader
}
}
// 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 @@ class DebugClassLoader
}
}
// Method from a trait
if ($method->getFilename() !== $refl->getFileName()) {
// 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);
}
}
}

View File

@ -272,6 +272,24 @@ class DebugClassLoaderTest extends TestCase
'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait2::internalMethod()" method is considered internal. It may change without further notice. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".',
));
}
public function testExtendedMethodDefinesNewParameters()
{
$deprecations = array();
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
$e = error_reporting(E_USER_DEPRECATED);
class_exists(__NAMESPACE__.'\\Fixtures\SubClassWithAnnotatedParameters', true);
error_reporting($e);
restore_error_handler();
$this->assertSame(array(
'The "Symfony\Component\Debug\Tests\Fixtures\SubClassWithAnnotatedParameters::quzMethod()" method will require a new "Quz $quz" argument in the next major version of its parent class "Symfony\Component\Debug\Tests\Fixtures\ClassWithAnnotatedParameters", not defining it is deprecated.',
'The "Symfony\Component\Debug\Tests\Fixtures\SubClassWithAnnotatedParameters::whereAmI()" method will require a new "bool $matrix" argument in the next major version of its parent class "Symfony\Component\Debug\Tests\Fixtures\InterfaceWithAnnotatedParameters", not defining it is deprecated.',
'The "Symfony\Component\Debug\Tests\Fixtures\SubClassWithAnnotatedParameters::isSymfony()" method will require a new "true $yes" argument in the next major version of its parent class "Symfony\Component\Debug\Tests\Fixtures\ClassWithAnnotatedParameters", not defining it is deprecated.',
), $deprecations);
}
}
class ClassLoader

View File

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

View File

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

View File

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

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
trait TraitWithAnnotatedParameters
{
/**
* `@param` annotations in traits are not parsed.
*/
public function isSymfony()
{
}
}