Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7cef7f2

Browse files
GuilhemNnicolas-grekas
authored andcommitted
[Debug] Trigger a deprecation for new parameters not defined in sub classes
1 parent 0d9154e commit 7cef7f2

6 files changed

+158
-13
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\Debug;
1313

14+
use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation;
15+
1416
/**
1517
* Autoloader checking if the class is really defined in the file found.
1618
*
@@ -34,6 +36,7 @@ class DebugClassLoader
3436
private static $deprecated = array();
3537
private static $internal = array();
3638
private static $internalMethods = array();
39+
private static $annotatedParameters = array();
3740
private static $darwinCache = array('/' => array('/', array()));
3841

3942
public function __construct(callable $classLoader)
@@ -200,9 +203,12 @@ private function checkClass($class, $file = null)
200203
}
201204
}
202205

206+
$parent = \get_parent_class($class);
203207
$parentAndTraits = \class_uses($name, false);
204-
if ($parent = \get_parent_class($class)) {
205-
$parentAndTraits[] = $parent;
208+
$parentAndOwnInterfaces = $this->getOwnInterfaces($name, $parent);
209+
if ($parent) {
210+
$parentAndTraits[$parent] = $parent;
211+
$parentAndOwnInterfaces[$parent] = $parent;
206212

207213
if (!isset(self::$checkedClasses[$parent])) {
208214
$this->checkClass($parent);
@@ -214,7 +220,7 @@ private function checkClass($class, $file = null)
214220
}
215221

216222
// Detect if the parent is annotated
217-
foreach ($parentAndTraits + $this->getOwnInterfaces($name, $parent) as $use) {
223+
foreach ($parentAndTraits + $parentAndOwnInterfaces as $use) {
218224
if (!isset(self::$checkedClasses[$use])) {
219225
$this->checkClass($use);
220226
}
@@ -229,11 +235,17 @@ private function checkClass($class, $file = null)
229235
}
230236
}
231237

232-
// Inherit @final and @internal annotations for methods
238+
// Inherit @final, @internal and @param annotations for methods
233239
self::$finalMethods[$name] = array();
234240
self::$internalMethods[$name] = array();
235-
foreach ($parentAndTraits as $use) {
236-
foreach (array('finalMethods', 'internalMethods') as $property) {
241+
self::$annotatedParameters[$name] = array();
242+
$map = array(
243+
'finalMethods' => $parentAndTraits,
244+
'internalMethods' => $parentAndTraits,
245+
'annotatedParameters' => $parentAndOwnInterfaces, // We don't parse traits params
246+
);
247+
foreach ($map as $property => $uses) {
248+
foreach ($uses as $use) {
237249
if (isset(self::${$property}[$use])) {
238250
self::${$property}[$name] = self::${$property}[$name] ? self::${$property}[$use] + self::${$property}[$name] : self::${$property}[$use];
239251
}
@@ -258,20 +270,50 @@ private function checkClass($class, $file = null)
258270
}
259271
}
260272

261-
// Method from a trait
262-
if ($method->getFilename() !== $refl->getFileName()) {
263-
continue;
273+
if (isset(self::$annotatedParameters[$name][$method->name])) {
274+
$definedParameters = array();
275+
foreach ($method->getParameters() as $parameter) {
276+
$definedParameters[$parameter->name] = true;
277+
}
278+
279+
foreach (array_diff_key(self::$annotatedParameters[$name][$method->name], $definedParameters) as $deprecation) {
280+
@trigger_error(sprintf($deprecation, $name), E_USER_DEPRECATED);
281+
}
264282
}
265283

266284
// Detect method annotations
267285
if (false === $doc = $method->getDocComment()) {
268286
continue;
269287
}
270288

271-
foreach (array('final', 'internal') as $annotation) {
272-
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
273-
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
274-
self::${$annotation.'Methods'}[$name][$method->name] = array($name, $message);
289+
$finalOrInternal = false;
290+
291+
// Skip methods from traits
292+
if ($method->getFilename() === $refl->getFileName()) {
293+
foreach (array('final', 'internal') as $annotation) {
294+
if (false !== \strpos($doc, $annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
295+
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
296+
self::${$annotation.'Methods'}[$name][$method->name] = array($name, $message);
297+
$finalOrInternal = true;
298+
}
299+
}
300+
}
301+
302+
if ($finalOrInternal || $method->isConstructor() || false === \strpos($doc, '@param') || StatelessInvocation::class === $name) {
303+
continue;
304+
}
305+
306+
if (!preg_match_all('#\n\s+\* @param (.*?)(?<= )\$([a-zA-Z0-9_\x7f-\xff]++)#', $doc, $matches, PREG_SET_ORDER)) {
307+
continue;
308+
}
309+
$definedParameters = array();
310+
foreach ($method->getParameters() as $parameter) {
311+
$definedParameters[$parameter->name] = true;
312+
}
313+
foreach ($matches as list(, $parameterType, $parameterName)) {
314+
if (!isset($definedParameters[$parameterName])) {
315+
$parameterType = trim($parameterType);
316+
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);
275317
}
276318
}
277319
}

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,24 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsInternals', true);
272272
'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".',
273273
));
274274
}
275+
276+
public function testExtendedMethodDefinesNewParameters()
277+
{
278+
$deprecations = array();
279+
set_error_handler(function ($type, $msg) use (&$deprecations) { $deprecations[] = $msg; });
280+
$e = error_reporting(E_USER_DEPRECATED);
281+
282+
class_exists(__NAMESPACE__.'\\Fixtures\SubClassWithAnnotatedParameters', true);
283+
284+
error_reporting($e);
285+
restore_error_handler();
286+
287+
$this->assertSame(array(
288+
'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.',
289+
'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.',
290+
'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.',
291+
), $deprecations);
292+
}
275293
}
276294

277295
class ClassLoader
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+
class ClassWithAnnotatedParameters
6+
{
7+
/**
8+
* @param string $foo this is a foo parameter
9+
*/
10+
public function fooMethod(string $foo)
11+
{
12+
}
13+
14+
/**
15+
* @param string $bar parameter not implemented yet
16+
*/
17+
public function barMethod(/* string $bar = null */)
18+
{
19+
}
20+
21+
/**
22+
* @param Quz $quz parameter not implemented yet
23+
*/
24+
public function quzMethod(/* Quz $quz = null */)
25+
{
26+
}
27+
28+
/**
29+
* @param true $yes
30+
*/
31+
public function isSymfony()
32+
{
33+
}
34+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
/**
6+
* Ensures a deprecation is triggered when a new parameter is not declared in child classes.
7+
*/
8+
interface InterfaceWithAnnotatedParameters
9+
{
10+
/**
11+
* @param bool $matrix
12+
*/
13+
public function whereAmI();
14+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
class SubClassWithAnnotatedParameters extends ClassWithAnnotatedParameters implements InterfaceWithAnnotatedParameters
6+
{
7+
use TraitWithAnnotatedParameters;
8+
9+
public function fooMethod(string $foo)
10+
{
11+
}
12+
13+
public function barMethod($bar = null)
14+
{
15+
}
16+
17+
public function quzMethod()
18+
{
19+
}
20+
21+
public function whereAmI()
22+
{
23+
}
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
trait TraitWithAnnotatedParameters
6+
{
7+
/**
8+
* `@param` annotations in traits are not parsed.
9+
*/
10+
public function isSymfony()
11+
{
12+
}
13+
}

0 commit comments

Comments
 (0)