diff --git a/src/Symfony/Bridge/Monolog/Logger.php b/src/Symfony/Bridge/Monolog/Logger.php index 531ec8875b003..1d896f4ac4f2f 100644 --- a/src/Symfony/Bridge/Monolog/Logger.php +++ b/src/Symfony/Bridge/Monolog/Logger.php @@ -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 @@ public function getLogs(/* Request $request = null */) /** * {@inheritdoc} + * + * @param Request|null $request */ public function countErrors(/* Request $request = null */) { diff --git a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php index 1a8c7edd85e64..a64bb0cc63ea3 100644 --- a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php @@ -58,6 +58,8 @@ public function __invoke(array $record) /** * {@inheritdoc} + * + * @param Request|null $request */ public function getLogs(/* Request $request = null */) { @@ -78,6 +80,8 @@ public function getLogs(/* Request $request = null */) /** * {@inheritdoc} + * + * @param Request|null $request */ public function countErrors(/* Request $request = null */) { diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index b84c22df3c4c5..a378db338708d 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -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 * @author Christophe Coevoet * @author Nicolas Grekas + * @author Guilhem Niot */ 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) } } + $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()) { + // 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); } } } diff --git a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php index d323d795b930e..e89d3ffcaf33c 100644 --- a/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php +++ b/src/Symfony/Component/Debug/Tests/DebugClassLoaderTest.php @@ -272,6 +272,24 @@ class_exists('Test\\'.__NAMESPACE__.'\\ExtendsInternals', true); '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 diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/ClassWithAnnotatedParameters.php b/src/Symfony/Component/Debug/Tests/Fixtures/ClassWithAnnotatedParameters.php new file mode 100644 index 0000000000000..d6eec9aa69034 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/ClassWithAnnotatedParameters.php @@ -0,0 +1,34 @@ +