From f0c12234b98f884aedd1d767fcfbbbb3786f1d45 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 20 Dec 2017 20:11:52 +0100 Subject: [PATCH] [Debug] Skip DebugClassLoader checks for already parsed files --- .../Component/Debug/DebugClassLoader.php | 63 +++++++++++++------ .../Debug/Tests/phpt/debug_class_loader.phpt | 26 ++++++++ 2 files changed, 70 insertions(+), 19 deletions(-) create mode 100644 src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 9ff826ed09..78bfb85388 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -28,12 +28,13 @@ class DebugClassLoader private $isFinder; private $loaded = array(); private static $caseCheck; + private static $checkedClasses = array(); private static $final = array(); private static $finalMethods = array(); private static $deprecated = array(); private static $internal = array(); private static $internalMethods = array(); - private static $php7Reserved = array('int', 'float', 'bool', 'string', 'true', 'false', 'null'); + private static $php7Reserved = array('int' => 1, 'float' => 1, 'bool' => 1, 'string' => 1, 'true' => 1, 'false' => 1, 'null' => 1); private static $darwinCache = array('/' => array('/', array())); public function __construct(callable $classLoader) @@ -139,8 +140,14 @@ class DebugClassLoader try { if ($this->isFinder && !isset($this->loaded[$class])) { $this->loaded[$class] = true; - if ($file = $this->classLoader[0]->findFile($class)) { + if ($file = $this->classLoader[0]->findFile($class) ?: false) { + $wasCached = \function_exists('opcache_is_script_cached') && opcache_is_script_cached($file); + require $file; + + if ($wasCached) { + return; + } } } else { call_user_func($this->classLoader, $class); @@ -150,41 +157,58 @@ class DebugClassLoader error_reporting($e); } - $exists = class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); + $this->checkClass($class, $file); + } - if ($class && '\\' === $class[0]) { + private function checkClass($class, $file = null) + { + $exists = null === $file || \class_exists($class, false) || \interface_exists($class, false) || \trait_exists($class, false); + + if (null !== $file && $class && '\\' === $class[0]) { $class = substr($class, 1); } if ($exists) { + if (isset(self::$checkedClasses[$class])) { + return; + } + self::$checkedClasses[$class] = true; + $refl = new \ReflectionClass($class); + if (null === $file && $refl->isInternal()) { + return; + } $name = $refl->getName(); - if ($name !== $class && 0 === strcasecmp($name, $class)) { + if ($name !== $class && 0 === \strcasecmp($name, $class)) { throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); } // Don't trigger deprecations for classes in the same vendor - if (2 > $len = 1 + (strpos($name, '\\') ?: strpos($name, '_'))) { + if (2 > $len = 1 + (\strpos($name, '\\') ?: \strpos($name, '_'))) { $len = 0; $ns = ''; } else { - $ns = substr($name, 0, $len); + $ns = \substr($name, 0, $len); } // Detect annotations on the class if (false !== $doc = $refl->getDocComment()) { foreach (array('final', 'deprecated', 'internal') as $annotation) { - if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n \* @'.$annotation.'(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) { + if (false !== \strpos($doc, $annotation) && preg_match('#\n \* @'.$annotation.'(?:( .+?)\.?)?\r?\n \*(?: @|/$)#s', $doc, $notice)) { self::${$annotation}[$name] = isset($notice[1]) ? preg_replace('#\s*\r?\n \* +#', ' ', $notice[1]) : ''; } } } - $parentAndTraits = class_uses($name, false); - if ($parent = get_parent_class($class)) { + $parentAndTraits = \class_uses($name, false); + if ($parent = \get_parent_class($class)) { $parentAndTraits[] = $parent; + if (!isset(self::$checkedClasses[$parent])) { + $this->checkClass($parent); + } + if (isset(self::$final[$parent])) { @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); } @@ -192,13 +216,16 @@ class DebugClassLoader // Detect if the parent is annotated foreach ($parentAndTraits + $this->getOwnInterfaces($name, $parent) as $use) { - if (isset(self::$deprecated[$use]) && strncmp($ns, $use, $len)) { + if (!isset(self::$checkedClasses[$use])) { + $this->checkClass($use); + } + if (isset(self::$deprecated[$use]) && \strncmp($ns, $use, $len)) { $type = class_exists($name, false) ? 'class' : (interface_exists($name, false) ? 'interface' : 'trait'); $verb = class_exists($use, false) || interface_exists($name, false) ? 'extends' : (interface_exists($use, false) ? 'implements' : 'uses'); @trigger_error(sprintf('The "%s" %s %s "%s" that is deprecated%s.', $name, $type, $verb, $use, self::$deprecated[$use]), E_USER_DEPRECATED); } - if (isset(self::$internal[$use]) && strncmp($ns, $use, $len)) { + if (isset(self::$internal[$use]) && \strncmp($ns, $use, $len)) { @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); } } @@ -209,12 +236,12 @@ class DebugClassLoader foreach ($parentAndTraits as $use) { foreach (array('finalMethods', 'internalMethods') as $property) { if (isset(self::${$property}[$use])) { - self::${$property}[$name] = array_merge(self::${$property}[$name], self::${$property}[$use]); + self::${$property}[$name] = self::${$property}[$name] ? self::${$property}[$use] + self::${$property}[$name] : self::${$property}[$use]; } } } - $isClass = class_exists($name, false); + $isClass = \class_exists($name, false); foreach ($refl->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $method) { if ($method->class !== $name) { continue; @@ -233,7 +260,7 @@ class DebugClassLoader foreach ($parentAndTraits as $use) { if (isset(self::$internalMethods[$use][$method->name])) { list($declaringClass, $message) = self::$internalMethods[$use][$method->name]; - if (strncmp($ns, $declaringClass, $len)) { + if (\strncmp($ns, $declaringClass, $len)) { @trigger_error(sprintf('The "%s::%s()" method is considered internal%s. It may change without further notice. You should not extend it from "%s".', $declaringClass, $method->name, $message, $name), E_USER_DEPRECATED); } } @@ -245,14 +272,14 @@ class DebugClassLoader } foreach (array('final', 'internal') as $annotation) { - if (false !== strpos($doc, '@'.$annotation) && preg_match('#\n\s+\* @'.$annotation.'(?:( .+?)\.?)?\r?\n\s+\*(?: @|/$)#s', $doc, $notice)) { + 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 (in_array(strtolower($refl->getShortName()), self::$php7Reserved)) { + if (isset(self::$php7Reserved[\strtolower($refl->getShortName())])) { @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); } } @@ -350,8 +377,6 @@ class DebugClassLoader throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1))); } } - - return true; } } diff --git a/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt b/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt new file mode 100644 index 0000000000..9525b08d68 --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/phpt/debug_class_loader.phpt @@ -0,0 +1,26 @@ +--TEST-- +Test DebugClassLoader with previoulsy loaded parents +--FILE-- + +--EXPECTF-- +The "Symfony\Component\Debug\Tests\Fixtures\FinalMethod::finalMethod()" method is considered final since version 3.3. It may change without further notice as of its next major version. You should not extend it from "Symfony\Component\Debug\Tests\Fixtures\ExtendedFinalMethod".