Skip to content

Commit 0891882

Browse files
committed
[Debug] Detect internal and deprecated methods
1 parent 0effd27 commit 0891882

File tree

5 files changed

+154
-66
lines changed

5 files changed

+154
-66
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

+95-66
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ class DebugClassLoader
2727
private $classLoader;
2828
private $isFinder;
2929
private static $caseCheck;
30-
private static $internal = array();
3130
private static $final = array();
3231
private static $finalMethods = array();
3332
private static $deprecated = array();
33+
private static $deprecatedMethods = array();
34+
private static $internal = array();
35+
private static $internalMethods = array();
3436
private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null');
3537
private static $darwinCache = array('/' => array('/', array()));
3638

@@ -166,53 +168,6 @@ public function loadClass($class)
166168
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name));
167169
}
168170

169-
$parent = get_parent_class($class);
170-
$doc = $refl->getDocComment();
171-
if (preg_match('#\n \* @internal(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) {
172-
self::$internal[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
173-
}
174-
175-
// Not an interface nor a trait
176-
if (class_exists($name, false)) {
177-
if (preg_match('#\n \* @final(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) {
178-
self::$final[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
179-
}
180-
181-
if ($parent && isset(self::$final[$parent])) {
182-
@trigger_error(sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $name), E_USER_DEPRECATED);
183-
}
184-
185-
// Inherit @final annotations
186-
self::$finalMethods[$name] = $parent && isset(self::$finalMethods[$parent]) ? self::$finalMethods[$parent] : array();
187-
188-
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
189-
if ($method->class !== $name) {
190-
continue;
191-
}
192-
193-
if ($parent && isset(self::$finalMethods[$parent][$method->name])) {
194-
@trigger_error(sprintf('%s It may change without further notice as of its next major version. You should not extend it from "%s".', self::$finalMethods[$parent][$method->name], $name), E_USER_DEPRECATED);
195-
}
196-
197-
$doc = $method->getDocComment();
198-
if (false === $doc || false === strpos($doc, '@final')) {
199-
continue;
200-
}
201-
202-
if (preg_match('#\n\s+\* @final(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
203-
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
204-
self::$finalMethods[$name][$method->name] = sprintf('The "%s::%s()" method is considered final%s.', $name, $method->name, $message);
205-
}
206-
}
207-
}
208-
209-
if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) {
210-
@trigger_error(sprintf('The "%s" class uses the reserved name "%s", it will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED);
211-
}
212-
if (preg_match('#\n \* @deprecated (.*?)\r?\n \*(?: @|/$)#s', $refl->getDocComment(), $notice)) {
213-
self::$deprecated[$name] = preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]);
214-
}
215-
216171
// Don't trigger deprecations for classes in the same vendor
217172
if (2 > $len = 1 + (strpos($name, '\\', 1 + strpos($name, '\\')) ?: strpos($name, '_'))) {
218173
$len = 0;
@@ -228,40 +183,88 @@ public function loadClass($class)
228183
}
229184
}
230185

231-
foreach (array_merge(array($parent), class_implements($name, false), class_uses($name, false)) as $use) {
186+
// Detect annotations on the class
187+
if (false !== $doc = $refl->getDocComment()) {
188+
foreach (array('final', 'deprecated', 'internal') as $annotation) {
189+
if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n \* @'.$annotation.'(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) {
190+
self::${$annotation}[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
191+
}
192+
}
193+
}
194+
195+
if ($parent = get_parent_class($class)) {
196+
if (isset(self::$final[$parent])) {
197+
@trigger_error(sprintf('The "%s" class is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $parent, self::$final[$parent], $name), E_USER_DEPRECATED);
198+
}
199+
}
200+
201+
$traits = class_uses($name, false);
202+
$interfaces = $this->getOwnInterfaces($name);
203+
204+
// Detect if the parent is annotated
205+
foreach (array_merge(array($parent), $interfaces, $traits) as $use) {
206+
if (isset(self::$deprecated[$use]) && strncmp($ns, $use, $len)) {
207+
$type = class_exists($name, false) ? 'class' : (interface_exists($name, false) ? 'interface' : 'trait');
208+
$verb = class_exists($use, false) || interface_exists($name, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses');
209+
210+
@trigger_error(sprintf('The "%s" %s %s "%s" that is deprecated%s.', $name, $type, $verb, $use, self::$deprecated[$use]), E_USER_DEPRECATED);
211+
}
232212
if (isset(self::$internal[$use]) && strncmp($ns, $use, $len)) {
233213
@trigger_error(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], $name), E_USER_DEPRECATED);
234214
}
235215
}
236216

237-
if (!$parent || strncmp($ns, $parent, $len)) {
238-
if ($parent && isset(self::$deprecated[$parent]) && strncmp($ns, $parent, $len)) {
239-
@trigger_error(sprintf('The "%s" class extends "%s" that is deprecated %s', $name, $parent, self::$deprecated[$parent]), E_USER_DEPRECATED);
217+
// Inherit @final and @deprecated annotations for methods
218+
self::$finalMethods[$name] = array();
219+
self::$deprecatedMethods[$name] = array();
220+
self::$internalMethods[$name] = array();
221+
foreach (array_merge(array($parent), $traits) as $use) {
222+
foreach (array('finalMethods', 'deprecatedMethods', 'internalMethods') as $property) {
223+
if (isset(self::${$property}[$use])) {
224+
self::${$property}[$name] = array_merge(self::${$property}[$name], self::${$property}[$use]);
225+
}
226+
}
227+
}
228+
229+
$isClass = class_exists($name, false);
230+
foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) {
231+
if ($method->class !== $name) {
232+
continue;
240233
}
241234

242-
$parentInterfaces = array();
243-
$deprecatedInterfaces = array();
244-
if ($parent) {
245-
foreach (class_implements($parent) as $interface) {
246-
$parentInterfaces[$interface] = 1;
247-
}
235+
if ($isClass && $parent && isset(self::$finalMethods[$parent][$method->name])) {
236+
list($methodShortName, $message) = self::$finalMethods[$parent][$method->name];
237+
@trigger_error(sprintf('The "%s" method is considered final%s. It may change without further notice as of its next major version. You should not extend it from "%s".', $methodShortName, $message, $name), E_USER_DEPRECATED);
248238
}
249239

250-
foreach ($refl->getInterfaceNames() as $interface) {
251-
if (isset(self::$deprecated[$interface]) && strncmp($ns, $interface, $len)) {
252-
$deprecatedInterfaces[] = $interface;
240+
foreach (array_merge(array($parent), $traits) as $use) {
241+
if (isset(self::$deprecatedMethods[$use][$method->name]) && strncmp($ns, $use, $len)) {
242+
list($methodShortName, $message) = self::$deprecatedMethods[$use][$method->name];
243+
@trigger_error(sprintf('The "%s" method is deprecated%s. You should not extend it from "%s".', $methodShortName, $message, $name), E_USER_DEPRECATED);
253244
}
254-
foreach (class_implements($interface) as $interface) {
255-
$parentInterfaces[$interface] = 1;
245+
if (isset(self::$internalMethods[$use][$method->name]) && strncmp($ns, $use, $len)) {
246+
list($methodShortName, $message) = self::$internalMethods[$use][$method->name];
247+
@trigger_error(sprintf('The "%s" method is considered internal%s. It may change without further notice. You should not use it from "%s".', $methodShortName, $message, $name), E_USER_DEPRECATED);
256248
}
257249
}
258250

259-
foreach ($deprecatedInterfaces as $interface) {
260-
if (!isset($parentInterfaces[$interface])) {
261-
@trigger_error(sprintf('The "%s" %s "%s" that is deprecated %s', $name, $refl->isInterface() ? 'interface extends' : 'class implements', $interface, self::$deprecated[$interface]), E_USER_DEPRECATED);
251+
// Detect method annotations
252+
$doc = $method->getDocComment();
253+
if (false === $doc) {
254+
continue;
255+
}
256+
257+
foreach (array('final', 'deprecated', 'internal') as $annotation) {
258+
if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) {
259+
$message = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : '';
260+
self::${$annotation.'Methods'}[$name][$method->name] = array(sprintf('%s::%s()', $name, $method->name), $message);
262261
}
263262
}
264263
}
264+
265+
if (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) {
266+
@trigger_error(sprintf('The "%s" class uses the reserved name "%s", it will break on PHP 7 and higher', $name, $refl->getShortName()), E_USER_DEPRECATED);
267+
}
265268
}
266269

267270
if ($file) {
@@ -361,4 +364,30 @@ public function loadClass($class)
361364
return true;
362365
}
363366
}
367+
368+
/**
369+
* `class_implements` includes interfaces from the parents so we have to
370+
* manually exclude them.
371+
*
372+
* @param string $class
373+
*
374+
* @return string[]
375+
*/
376+
private function getOwnInterfaces($class)
377+
{
378+
$parentInterfaces = array();
379+
if ($parent = get_parent_class($class)) {
380+
foreach (class_implements($parent, false) as $interface) {
381+
$parentInterfaces[$interface] = true;
382+
}
383+
}
384+
385+
foreach (class_implements($class, false) as $interface) {
386+
foreach (class_implements($interface) as $interface) {
387+
$parentInterfaces[$interface] = true;
388+
}
389+
}
390+
391+
return array_keys(array_diff_key(class_implements($class, false), $parentInterfaces));
392+
}
364393
}

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

+29
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,28 @@ class_exists(__NAMESPACE__.'\\Fixtures\\ExtendedFinalMethod', true);
313313
$this->assertSame($xError, $lastError);
314314
}
315315

316+
public function testExtendedDeprecatedMethod()
317+
{
318+
set_error_handler(function () { return false; });
319+
$e = error_reporting(0);
320+
trigger_error('', E_USER_NOTICE);
321+
322+
class_exists('Test\\'.__NAMESPACE__.'\\ExtendsAnnotatedClass', true);
323+
324+
error_reporting($e);
325+
restore_error_handler();
326+
327+
$lastError = error_get_last();
328+
unset($lastError['file'], $lastError['line']);
329+
330+
$xError = array(
331+
'type' => E_USER_DEPRECATED,
332+
'message' => 'The "Symfony\Component\Debug\Tests\Fixtures\AnnotatedClass::deprecatedMethod()" method is deprecated since version 3.4. You should not extend it from "Test\Symfony\Component\Debug\Tests\ExtendsAnnotatedClass".',
333+
);
334+
335+
$this->assertSame($xError, $lastError);
336+
}
337+
316338
public function testInternalsUse()
317339
{
318340
$deprecations = array();
@@ -328,6 +350,7 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsInternals', true);
328350
'The "Symfony\Component\Debug\Tests\Fixtures\InternalClass" class is considered internal since version 3.4. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".',
329351
'The "Symfony\Component\Debug\Tests\Fixtures\InternalInterface" interface is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".',
330352
'The "Symfony\Component\Debug\Tests\Fixtures\InternalTrait" trait is considered internal. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".',
353+
'The "Symfony\Component\Debug\Tests\Fixtures\InternalClass::internalMethod()" method is considered internal since version 3.4. It may change without further notice. You should not use it from "Test\Symfony\Component\Debug\Tests\ExtendsInternals".',
331354
));
332355
}
333356
}
@@ -371,9 +394,15 @@ public function findFile($class)
371394
eval('namespace Test\\'.__NAMESPACE__.'; class Float {}');
372395
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsFinalClass' === $class) {
373396
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsFinalClass extends \\'.__NAMESPACE__.'\Fixtures\FinalClass {}');
397+
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsAnnotatedClass' === $class) {
398+
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsAnnotatedClass extends \\'.__NAMESPACE__.'\Fixtures\AnnotatedClass {
399+
public function deprecatedMethod() { }
400+
}');
374401
} elseif ('Test\\'.__NAMESPACE__.'\ExtendsInternals' === $class) {
375402
eval('namespace Test\\'.__NAMESPACE__.'; class ExtendsInternals extends \\'.__NAMESPACE__.'\Fixtures\InternalClass implements \\'.__NAMESPACE__.'\Fixtures\InternalInterface {
376403
use \\'.__NAMESPACE__.'\Fixtures\InternalTrait;
404+
405+
public function internalMethod() { }
377406
}');
378407
}
379408
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
class AnnotatedClass
6+
{
7+
/**
8+
* @deprecated since version 3.4.
9+
*/
10+
public function deprecatedMethod()
11+
{
12+
}
13+
}

src/Symfony/Component/Debug/Tests/Fixtures/InternalClass.php

+4
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@
88
class InternalClass
99
{
1010
use InternalTrait2;
11+
12+
public function usedInInternalClass()
13+
{
14+
}
1115
}

src/Symfony/Component/Debug/Tests/Fixtures/InternalTrait2.php

+13
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,17 @@
77
*/
88
trait InternalTrait2
99
{
10+
/**
11+
* @internal since version 3.4
12+
*/
13+
public function internalMethod()
14+
{
15+
}
16+
17+
/**
18+
* @internal but should not trigger a deprecation.
19+
*/
20+
public function usedInInternalClass()
21+
{
22+
}
1023
}

0 commit comments

Comments
 (0)