Error
1 year ago
ErrorEnhancer
1 year ago
ErrorRenderer
1 year ago
Exception
1 year ago
Internal
2 years ago
Resources
1 year ago
BufferingLogger.php
1 year ago
Debug.php
1 year ago
DebugClassLoader.php
1 year ago
ErrorHandler.php
1 year ago
LICENSE
2 years ago
README.md
2 years ago
ThrowableUtils.php
2 years ago
DebugClassLoader.php
943 lines
| 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * This file is part of the Symfony package. |
| 5 | * |
| 6 | * (c) Fabien Potencier <fabien@symfony.com> |
| 7 | * |
| 8 | * For the full copyright and license information, please view the LICENSE |
| 9 | * file that was distributed with this source code. |
| 10 | */ |
| 11 | namespace Matomo\Dependencies\Symfony\Component\ErrorHandler; |
| 12 | |
| 13 | use Composer\InstalledVersions; |
| 14 | use Doctrine\Common\Persistence\Proxy as LegacyProxy; |
| 15 | use Doctrine\Persistence\Proxy; |
| 16 | use Mockery\MockInterface; |
| 17 | use Phake\IMock; |
| 18 | use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; |
| 19 | use PHPUnit\Framework\MockObject\MockObject; |
| 20 | use Prophecy\Prophecy\ProphecySubjectInterface; |
| 21 | use ProxyManager\Proxy\ProxyInterface; |
| 22 | use Matomo\Dependencies\Symfony\Component\ErrorHandler\Internal\TentativeTypes; |
| 23 | use Symfony\Component\HttpClient\HttplugClient; |
| 24 | /** |
| 25 | * Autoloader checking if the class is really defined in the file found. |
| 26 | * |
| 27 | * The ClassLoader will wrap all registered autoloaders |
| 28 | * and will throw an exception if a file is found but does |
| 29 | * not declare the class. |
| 30 | * |
| 31 | * It can also patch classes to turn docblocks into actual return types. |
| 32 | * This behavior is controlled by the SYMFONY_PATCH_TYPE_DECLARATIONS env var, |
| 33 | * which is a url-encoded array with the follow parameters: |
| 34 | * - "force": any value enables deprecation notices - can be any of: |
| 35 | * - "phpdoc" to patch only docblock annotations |
| 36 | * - "2" to add all possible return types |
| 37 | * - "1" to add return types but only to tests/final/internal/private methods |
| 38 | * - "php": the target version of PHP - e.g. "7.1" doesn't generate "object" types |
| 39 | * - "deprecations": "1" to trigger a deprecation notice when a child class misses a |
| 40 | * return type while the parent declares an "@return" annotation |
| 41 | * |
| 42 | * Note that patching doesn't care about any coding style so you'd better to run |
| 43 | * php-cs-fixer after, with rules "phpdoc_trim_consecutive_blank_line_separation" |
| 44 | * and "no_superfluous_phpdoc_tags" enabled typically. |
| 45 | * |
| 46 | * @author Fabien Potencier <fabien@symfony.com> |
| 47 | * @author Christophe Coevoet <stof@notk.org> |
| 48 | * @author Nicolas Grekas <p@tchwork.com> |
| 49 | * @author Guilhem Niot <guilhem.niot@gmail.com> |
| 50 | */ |
| 51 | class DebugClassLoader |
| 52 | { |
| 53 | private const SPECIAL_RETURN_TYPES = ['void' => 'void', 'null' => 'null', 'resource' => 'resource', 'boolean' => 'bool', 'true' => 'true', 'false' => 'false', 'integer' => 'int', 'array' => 'array', 'bool' => 'bool', 'callable' => 'callable', 'float' => 'float', 'int' => 'int', 'iterable' => 'iterable', 'object' => 'object', 'string' => 'string', 'self' => 'self', 'parent' => 'parent', 'mixed' => 'mixed', 'static' => 'static', '$this' => 'static', 'list' => 'array', 'class-string' => 'string', 'never' => 'never']; |
| 54 | private const BUILTIN_RETURN_TYPES = ['void' => \true, 'array' => \true, 'false' => \true, 'bool' => \true, 'callable' => \true, 'float' => \true, 'int' => \true, 'iterable' => \true, 'object' => \true, 'string' => \true, 'self' => \true, 'parent' => \true, 'mixed' => \true, 'static' => \true, 'null' => \true, 'true' => \true, 'never' => \true]; |
| 55 | private const MAGIC_METHODS = ['__isset' => 'bool', '__sleep' => 'array', '__toString' => 'string', '__debugInfo' => 'array', '__serialize' => 'array']; |
| 56 | private $classLoader; |
| 57 | private $isFinder; |
| 58 | private $loaded = []; |
| 59 | private $patchTypes; |
| 60 | private static $caseCheck; |
| 61 | private static $checkedClasses = []; |
| 62 | private static $final = []; |
| 63 | private static $finalMethods = []; |
| 64 | private static $deprecated = []; |
| 65 | private static $internal = []; |
| 66 | private static $internalMethods = []; |
| 67 | private static $annotatedParameters = []; |
| 68 | private static $darwinCache = ['/' => ['/', []]]; |
| 69 | private static $method = []; |
| 70 | private static $returnTypes = []; |
| 71 | private static $methodTraits = []; |
| 72 | private static $fileOffsets = []; |
| 73 | public function __construct(callable $classLoader) |
| 74 | { |
| 75 | $this->classLoader = $classLoader; |
| 76 | $this->isFinder = \is_array($classLoader) && method_exists($classLoader[0], 'findFile'); |
| 77 | parse_str(getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') ?: '', $this->patchTypes); |
| 78 | $this->patchTypes += ['force' => null, 'php' => \PHP_MAJOR_VERSION . '.' . \PHP_MINOR_VERSION, 'deprecations' => \PHP_VERSION_ID >= 70400]; |
| 79 | if ('phpdoc' === $this->patchTypes['force']) { |
| 80 | $this->patchTypes['force'] = 'docblock'; |
| 81 | } |
| 82 | if (!isset(self::$caseCheck)) { |
| 83 | $file = is_file(__FILE__) ? __FILE__ : rtrim(realpath('.'), \DIRECTORY_SEPARATOR); |
| 84 | $i = strrpos($file, \DIRECTORY_SEPARATOR); |
| 85 | $dir = substr($file, 0, 1 + $i); |
| 86 | $file = substr($file, 1 + $i); |
| 87 | $test = strtoupper($file) === $file ? strtolower($file) : strtoupper($file); |
| 88 | $test = realpath($dir . $test); |
| 89 | if (\false === $test || \false === $i) { |
| 90 | // filesystem is case sensitive |
| 91 | self::$caseCheck = 0; |
| 92 | } elseif (substr($test, -\strlen($file)) === $file) { |
| 93 | // filesystem is case insensitive and realpath() normalizes the case of characters |
| 94 | self::$caseCheck = 1; |
| 95 | } elseif ('Darwin' === \PHP_OS_FAMILY) { |
| 96 | // on MacOSX, HFS+ is case insensitive but realpath() doesn't normalize the case of characters |
| 97 | self::$caseCheck = 2; |
| 98 | } else { |
| 99 | // filesystem case checks failed, fallback to disabling them |
| 100 | self::$caseCheck = 0; |
| 101 | } |
| 102 | } |
| 103 | } |
| 104 | public function getClassLoader() : callable |
| 105 | { |
| 106 | return $this->classLoader; |
| 107 | } |
| 108 | /** |
| 109 | * Wraps all autoloaders. |
| 110 | */ |
| 111 | public static function enable() : void |
| 112 | { |
| 113 | // Ensures we don't hit https://bugs.php.net/42098 |
| 114 | class_exists(\Matomo\Dependencies\Symfony\Component\ErrorHandler\ErrorHandler::class); |
| 115 | class_exists(\Matomo\Dependencies\Psr\Log\LogLevel::class); |
| 116 | if (!\is_array($functions = spl_autoload_functions())) { |
| 117 | return; |
| 118 | } |
| 119 | foreach ($functions as $function) { |
| 120 | spl_autoload_unregister($function); |
| 121 | } |
| 122 | foreach ($functions as $function) { |
| 123 | if (!\is_array($function) || !$function[0] instanceof self) { |
| 124 | $function = [new static($function), 'loadClass']; |
| 125 | } |
| 126 | spl_autoload_register($function); |
| 127 | } |
| 128 | } |
| 129 | /** |
| 130 | * Disables the wrapping. |
| 131 | */ |
| 132 | public static function disable() : void |
| 133 | { |
| 134 | if (!\is_array($functions = spl_autoload_functions())) { |
| 135 | return; |
| 136 | } |
| 137 | foreach ($functions as $function) { |
| 138 | spl_autoload_unregister($function); |
| 139 | } |
| 140 | foreach ($functions as $function) { |
| 141 | if (\is_array($function) && $function[0] instanceof self) { |
| 142 | $function = $function[0]->getClassLoader(); |
| 143 | } |
| 144 | spl_autoload_register($function); |
| 145 | } |
| 146 | } |
| 147 | public static function checkClasses() : bool |
| 148 | { |
| 149 | if (!\is_array($functions = spl_autoload_functions())) { |
| 150 | return \false; |
| 151 | } |
| 152 | $loader = null; |
| 153 | foreach ($functions as $function) { |
| 154 | if (\is_array($function) && $function[0] instanceof self) { |
| 155 | $loader = $function[0]; |
| 156 | break; |
| 157 | } |
| 158 | } |
| 159 | if (null === $loader) { |
| 160 | return \false; |
| 161 | } |
| 162 | static $offsets = ['get_declared_interfaces' => 0, 'get_declared_traits' => 0, 'get_declared_classes' => 0]; |
| 163 | foreach ($offsets as $getSymbols => $i) { |
| 164 | $symbols = $getSymbols(); |
| 165 | for (; $i < \count($symbols); ++$i) { |
| 166 | if (!is_subclass_of($symbols[$i], MockObject::class) && !is_subclass_of($symbols[$i], ProphecySubjectInterface::class) && !is_subclass_of($symbols[$i], Proxy::class) && !is_subclass_of($symbols[$i], ProxyInterface::class) && !is_subclass_of($symbols[$i], LegacyProxy::class) && !is_subclass_of($symbols[$i], MockInterface::class) && !is_subclass_of($symbols[$i], IMock::class)) { |
| 167 | $loader->checkClass($symbols[$i]); |
| 168 | } |
| 169 | } |
| 170 | $offsets[$getSymbols] = $i; |
| 171 | } |
| 172 | return \true; |
| 173 | } |
| 174 | public function findFile(string $class) : ?string |
| 175 | { |
| 176 | return $this->isFinder ? $this->classLoader[0]->findFile($class) ?: null : null; |
| 177 | } |
| 178 | /** |
| 179 | * Loads the given class or interface. |
| 180 | * |
| 181 | * @throws \RuntimeException |
| 182 | */ |
| 183 | public function loadClass(string $class) : void |
| 184 | { |
| 185 | $e = error_reporting(error_reporting() | \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR); |
| 186 | try { |
| 187 | if ($this->isFinder && !isset($this->loaded[$class])) { |
| 188 | $this->loaded[$class] = \true; |
| 189 | if (!($file = $this->classLoader[0]->findFile($class) ?: '')) { |
| 190 | // no-op |
| 191 | } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) { |
| 192 | include $file; |
| 193 | return; |
| 194 | } elseif (\false === (include $file)) { |
| 195 | return; |
| 196 | } |
| 197 | } else { |
| 198 | ($this->classLoader)($class); |
| 199 | $file = ''; |
| 200 | } |
| 201 | } finally { |
| 202 | error_reporting($e); |
| 203 | } |
| 204 | $this->checkClass($class, $file); |
| 205 | } |
| 206 | private function checkClass(string $class, ?string $file = null) : void |
| 207 | { |
| 208 | $exists = null === $file || class_exists($class, \false) || interface_exists($class, \false) || trait_exists($class, \false); |
| 209 | if (null !== $file && $class && '\\' === $class[0]) { |
| 210 | $class = substr($class, 1); |
| 211 | } |
| 212 | if ($exists) { |
| 213 | if (isset(self::$checkedClasses[$class])) { |
| 214 | return; |
| 215 | } |
| 216 | self::$checkedClasses[$class] = \true; |
| 217 | $refl = new \ReflectionClass($class); |
| 218 | if (null === $file && $refl->isInternal()) { |
| 219 | return; |
| 220 | } |
| 221 | $name = $refl->getName(); |
| 222 | if ($name !== $class && 0 === strcasecmp($name, $class)) { |
| 223 | throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: "%s" vs "%s".', $class, $name)); |
| 224 | } |
| 225 | $deprecations = $this->checkAnnotations($refl, $name); |
| 226 | foreach ($deprecations as $message) { |
| 227 | @trigger_error($message, \E_USER_DEPRECATED); |
| 228 | } |
| 229 | } |
| 230 | if (!$file) { |
| 231 | return; |
| 232 | } |
| 233 | if (!$exists) { |
| 234 | if (\false !== strpos($class, '/')) { |
| 235 | throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\\" in PHP, not "/".', $class)); |
| 236 | } |
| 237 | throw new \RuntimeException(sprintf('The autoloader expected class "%s" to be defined in file "%s". The file was found but the class was not in it, the class name or namespace probably has a typo.', $class, $file)); |
| 238 | } |
| 239 | if (self::$caseCheck && ($message = $this->checkCase($refl, $file, $class))) { |
| 240 | throw new \RuntimeException(sprintf('Case mismatch between class and real file names: "%s" vs "%s" in "%s".', $message[0], $message[1], $message[2])); |
| 241 | } |
| 242 | } |
| 243 | public function checkAnnotations(\ReflectionClass $refl, string $class) : array |
| 244 | { |
| 245 | if ('Symfony\\Bridge\\PhpUnit\\Legacy\\SymfonyTestsListenerForV7' === $class || 'Symfony\\Bridge\\PhpUnit\\Legacy\\SymfonyTestsListenerForV6' === $class) { |
| 246 | return []; |
| 247 | } |
| 248 | $deprecations = []; |
| 249 | $className = \false !== strpos($class, "@anonymous\x00") ? ((get_parent_class($class) ?: key(class_implements($class))) ?: 'class') . '@anonymous' : $class; |
| 250 | // Don't trigger deprecations for classes in the same vendor |
| 251 | if ($class !== $className) { |
| 252 | $vendor = preg_match('/^namespace ([^;\\\\\\s]++)[;\\\\]/m', @file_get_contents($refl->getFileName()), $vendor) ? $vendor[1] . '\\' : ''; |
| 253 | $vendorLen = \strlen($vendor); |
| 254 | } elseif (2 > ($vendorLen = 1 + (strpos($class, '\\') ?: strpos($class, '_')))) { |
| 255 | $vendorLen = 0; |
| 256 | $vendor = ''; |
| 257 | } else { |
| 258 | $vendor = str_replace('_', '\\', substr($class, 0, $vendorLen)); |
| 259 | } |
| 260 | $parent = get_parent_class($class) ?: null; |
| 261 | self::$returnTypes[$class] = []; |
| 262 | $classIsTemplate = \false; |
| 263 | // Detect annotations on the class |
| 264 | if ($doc = $this->parsePhpDoc($refl)) { |
| 265 | $classIsTemplate = isset($doc['template']); |
| 266 | foreach (['final', 'deprecated', 'internal'] as $annotation) { |
| 267 | if (null !== ($description = $doc[$annotation][0] ?? null)) { |
| 268 | self::${$annotation}[$class] = '' !== $description ? ' ' . $description . (preg_match('/[.!]$/', $description) ? '' : '.') : '.'; |
| 269 | } |
| 270 | } |
| 271 | if ($refl->isInterface() && isset($doc['method'])) { |
| 272 | foreach ($doc['method'] as $name => [$static, $returnType, $signature, $description]) { |
| 273 | self::$method[$class][] = [$class, $static, $returnType, $name . $signature, $description]; |
| 274 | if ('' !== $returnType) { |
| 275 | $this->setReturnType($returnType, $refl->name, $name, $refl->getFileName(), $parent); |
| 276 | } |
| 277 | } |
| 278 | } |
| 279 | } |
| 280 | $parentAndOwnInterfaces = $this->getOwnInterfaces($class, $parent); |
| 281 | if ($parent) { |
| 282 | $parentAndOwnInterfaces[$parent] = $parent; |
| 283 | if (!isset(self::$checkedClasses[$parent])) { |
| 284 | $this->checkClass($parent); |
| 285 | } |
| 286 | if (isset(self::$final[$parent])) { |
| 287 | $deprecations[] = 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], $className); |
| 288 | } |
| 289 | } |
| 290 | // Detect if the parent is annotated |
| 291 | foreach ($parentAndOwnInterfaces + class_uses($class, \false) as $use) { |
| 292 | if (!isset(self::$checkedClasses[$use])) { |
| 293 | $this->checkClass($use); |
| 294 | } |
| 295 | if (isset(self::$deprecated[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && !isset(self::$deprecated[$class]) && !(HttplugClient::class === $class && \in_array($use, [\Http\Client\HttpClient::class, \Http\Message\RequestFactory::class, \Http\Message\StreamFactory::class, \Http\Message\UriFactory::class], \true))) { |
| 296 | $type = class_exists($class, \false) ? 'class' : (interface_exists($class, \false) ? 'interface' : 'trait'); |
| 297 | $verb = class_exists($use, \false) || interface_exists($class, \false) ? 'extends' : (interface_exists($use, \false) ? 'implements' : 'uses'); |
| 298 | $deprecations[] = sprintf('The "%s" %s %s "%s" that is deprecated%s', $className, $type, $verb, $use, self::$deprecated[$use]); |
| 299 | } |
| 300 | if (isset(self::$internal[$use]) && strncmp($vendor, str_replace('_', '\\', $use), $vendorLen)) { |
| 301 | $deprecations[] = 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], $className); |
| 302 | } |
| 303 | if (isset(self::$method[$use])) { |
| 304 | if ($refl->isAbstract()) { |
| 305 | if (isset(self::$method[$class])) { |
| 306 | self::$method[$class] = array_merge(self::$method[$class], self::$method[$use]); |
| 307 | } else { |
| 308 | self::$method[$class] = self::$method[$use]; |
| 309 | } |
| 310 | } elseif (!$refl->isInterface()) { |
| 311 | if (!strncmp($vendor, str_replace('_', '\\', $use), $vendorLen) && 0 === strpos($className, 'Symfony\\') && (!class_exists(InstalledVersions::class) || 'symfony/symfony' !== InstalledVersions::getRootPackage()['name'])) { |
| 312 | // skip "same vendor" @method deprecations for Symfony\* classes unless symfony/symfony is being tested |
| 313 | continue; |
| 314 | } |
| 315 | $hasCall = $refl->hasMethod('__call'); |
| 316 | $hasStaticCall = $refl->hasMethod('__callStatic'); |
| 317 | foreach (self::$method[$use] as [$interface, $static, $returnType, $name, $description]) { |
| 318 | if ($static ? $hasStaticCall : $hasCall) { |
| 319 | continue; |
| 320 | } |
| 321 | $realName = substr($name, 0, strpos($name, '(')); |
| 322 | if (!$refl->hasMethod($realName) || !($methodRefl = $refl->getMethod($realName))->isPublic() || $static && !$methodRefl->isStatic() || !$static && $methodRefl->isStatic()) { |
| 323 | $deprecations[] = sprintf('Class "%s" should implement method "%s::%s%s"%s', $className, ($static ? 'static ' : '') . $interface, $name, $returnType ? ': ' . $returnType : '', null === $description ? '.' : ': ' . $description); |
| 324 | } |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | } |
| 329 | if (trait_exists($class)) { |
| 330 | $file = $refl->getFileName(); |
| 331 | foreach ($refl->getMethods() as $method) { |
| 332 | if ($method->getFileName() === $file) { |
| 333 | self::$methodTraits[$file][$method->getStartLine()] = $class; |
| 334 | } |
| 335 | } |
| 336 | return $deprecations; |
| 337 | } |
| 338 | // Inherit @final, @internal, @param and @return annotations for methods |
| 339 | self::$finalMethods[$class] = []; |
| 340 | self::$internalMethods[$class] = []; |
| 341 | self::$annotatedParameters[$class] = []; |
| 342 | foreach ($parentAndOwnInterfaces as $use) { |
| 343 | foreach (['finalMethods', 'internalMethods', 'annotatedParameters', 'returnTypes'] as $property) { |
| 344 | if (isset(self::${$property}[$use])) { |
| 345 | self::${$property}[$class] = self::${$property}[$class] ? self::${$property}[$use] + self::${$property}[$class] : self::${$property}[$use]; |
| 346 | } |
| 347 | } |
| 348 | if (null !== (TentativeTypes::RETURN_TYPES[$use] ?? null)) { |
| 349 | foreach (TentativeTypes::RETURN_TYPES[$use] as $method => $returnType) { |
| 350 | $returnType = explode('|', $returnType); |
| 351 | foreach ($returnType as $i => $t) { |
| 352 | if ('?' !== $t && !isset(self::BUILTIN_RETURN_TYPES[$t])) { |
| 353 | $returnType[$i] = '\\' . $t; |
| 354 | } |
| 355 | } |
| 356 | $returnType = implode('|', $returnType); |
| 357 | self::$returnTypes[$class] += [$method => [$returnType, 0 === strpos($returnType, '?') ? substr($returnType, 1) . '|null' : $returnType, $use, '']]; |
| 358 | } |
| 359 | } |
| 360 | } |
| 361 | foreach ($refl->getMethods() as $method) { |
| 362 | if ($method->class !== $class) { |
| 363 | continue; |
| 364 | } |
| 365 | if (null === ($ns = self::$methodTraits[$method->getFileName()][$method->getStartLine()] ?? null)) { |
| 366 | $ns = $vendor; |
| 367 | $len = $vendorLen; |
| 368 | } elseif (2 > ($len = 1 + (strpos($ns, '\\') ?: strpos($ns, '_')))) { |
| 369 | $len = 0; |
| 370 | $ns = ''; |
| 371 | } else { |
| 372 | $ns = str_replace('_', '\\', substr($ns, 0, $len)); |
| 373 | } |
| 374 | if ($parent && isset(self::$finalMethods[$parent][$method->name])) { |
| 375 | [$declaringClass, $message] = self::$finalMethods[$parent][$method->name]; |
| 376 | $deprecations[] = sprintf('The "%s::%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".', $declaringClass, $method->name, $message, $className); |
| 377 | } |
| 378 | if (isset(self::$internalMethods[$class][$method->name])) { |
| 379 | [$declaringClass, $message] = self::$internalMethods[$class][$method->name]; |
| 380 | if (strncmp($ns, $declaringClass, $len)) { |
| 381 | $deprecations[] = 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, $className); |
| 382 | } |
| 383 | } |
| 384 | // To read method annotations |
| 385 | $doc = $this->parsePhpDoc($method); |
| 386 | if (($classIsTemplate || isset($doc['template'])) && $method->hasReturnType()) { |
| 387 | unset($doc['return']); |
| 388 | } |
| 389 | if (isset(self::$annotatedParameters[$class][$method->name])) { |
| 390 | $definedParameters = []; |
| 391 | foreach ($method->getParameters() as $parameter) { |
| 392 | $definedParameters[$parameter->name] = \true; |
| 393 | } |
| 394 | foreach (self::$annotatedParameters[$class][$method->name] as $parameterName => $deprecation) { |
| 395 | if (!isset($definedParameters[$parameterName]) && !isset($doc['param'][$parameterName])) { |
| 396 | $deprecations[] = sprintf($deprecation, $className); |
| 397 | } |
| 398 | } |
| 399 | } |
| 400 | $forcePatchTypes = $this->patchTypes['force']; |
| 401 | if ($canAddReturnType = null !== $forcePatchTypes && \false === strpos($method->getFileName(), \DIRECTORY_SEPARATOR . 'vendor' . \DIRECTORY_SEPARATOR)) { |
| 402 | if ('void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { |
| 403 | $this->patchTypes['force'] = $forcePatchTypes ?: 'docblock'; |
| 404 | } |
| 405 | $canAddReturnType = 2 === (int) $forcePatchTypes || \false !== stripos($method->getFileName(), \DIRECTORY_SEPARATOR . 'Tests' . \DIRECTORY_SEPARATOR) || $refl->isFinal() || $method->isFinal() || $method->isPrivate() || '.' === (self::$internal[$class] ?? null) && !$refl->isAbstract() || '.' === (self::$final[$class] ?? null) || '' === ($doc['final'][0] ?? null) || '' === ($doc['internal'][0] ?? null); |
| 406 | } |
| 407 | if (null !== ($returnType = self::$returnTypes[$class][$method->name] ?? null) && 'docblock' === $this->patchTypes['force'] && !$method->hasReturnType() && isset(TentativeTypes::RETURN_TYPES[$returnType[2]][$method->name])) { |
| 408 | $this->patchReturnTypeWillChange($method); |
| 409 | } |
| 410 | if (null !== ($returnType ?? ($returnType = self::MAGIC_METHODS[$method->name] ?? null)) && !$method->hasReturnType() && !isset($doc['return'])) { |
| 411 | [$normalizedType, $returnType, $declaringClass, $declaringFile] = \is_string($returnType) ? [$returnType, $returnType, '', ''] : $returnType; |
| 412 | if ($canAddReturnType && 'docblock' !== $this->patchTypes['force']) { |
| 413 | $this->patchMethod($method, $returnType, $declaringFile, $normalizedType); |
| 414 | } |
| 415 | if (!isset($doc['deprecated']) && strncmp($ns, $declaringClass, $len)) { |
| 416 | if ('docblock' === $this->patchTypes['force']) { |
| 417 | $this->patchMethod($method, $returnType, $declaringFile, $normalizedType); |
| 418 | } elseif ('' !== $declaringClass && $this->patchTypes['deprecations']) { |
| 419 | $deprecations[] = sprintf('Method "%s::%s()" might add "%s" as a native return type declaration in the future. Do the same in %s "%s" now to avoid errors or add an explicit @return annotation to suppress this message.', $declaringClass, $method->name, $normalizedType, interface_exists($declaringClass) ? 'implementation' : 'child class', $className); |
| 420 | } |
| 421 | } |
| 422 | } |
| 423 | if (!$doc) { |
| 424 | $this->patchTypes['force'] = $forcePatchTypes; |
| 425 | continue; |
| 426 | } |
| 427 | if (isset($doc['return']) || 'void' !== (self::MAGIC_METHODS[$method->name] ?? 'void')) { |
| 428 | $this->setReturnType($doc['return'] ?? self::MAGIC_METHODS[$method->name], $method->class, $method->name, $method->getFileName(), $parent, $method->getReturnType()); |
| 429 | if (isset(self::$returnTypes[$class][$method->name][0]) && $canAddReturnType) { |
| 430 | $this->fixReturnStatements($method, self::$returnTypes[$class][$method->name][0]); |
| 431 | } |
| 432 | if ($method->isPrivate()) { |
| 433 | unset(self::$returnTypes[$class][$method->name]); |
| 434 | } |
| 435 | } |
| 436 | $this->patchTypes['force'] = $forcePatchTypes; |
| 437 | if ($method->isPrivate()) { |
| 438 | continue; |
| 439 | } |
| 440 | $finalOrInternal = \false; |
| 441 | foreach (['final', 'internal'] as $annotation) { |
| 442 | if (null !== ($description = $doc[$annotation][0] ?? null)) { |
| 443 | self::${$annotation . 'Methods'}[$class][$method->name] = [$class, '' !== $description ? ' ' . $description . (preg_match('/[[:punct:]]$/', $description) ? '' : '.') : '.']; |
| 444 | $finalOrInternal = \true; |
| 445 | } |
| 446 | } |
| 447 | if ($finalOrInternal || $method->isConstructor() || !isset($doc['param']) || StatelessInvocation::class === $class) { |
| 448 | continue; |
| 449 | } |
| 450 | if (!isset(self::$annotatedParameters[$class][$method->name])) { |
| 451 | $definedParameters = []; |
| 452 | foreach ($method->getParameters() as $parameter) { |
| 453 | $definedParameters[$parameter->name] = \true; |
| 454 | } |
| 455 | } |
| 456 | foreach ($doc['param'] as $parameterName => $parameterType) { |
| 457 | if (!isset($definedParameters[$parameterName])) { |
| 458 | self::$annotatedParameters[$class][$method->name][$parameterName] = sprintf('The "%%s::%s()" method will require a new "%s$%s" argument in the next major version of its %s "%s", not defining it is deprecated.', $method->name, $parameterType ? $parameterType . ' ' : '', $parameterName, interface_exists($className) ? 'interface' : 'parent class', $className); |
| 459 | } |
| 460 | } |
| 461 | } |
| 462 | return $deprecations; |
| 463 | } |
| 464 | public function checkCase(\ReflectionClass $refl, string $file, string $class) : ?array |
| 465 | { |
| 466 | $real = explode('\\', $class . strrchr($file, '.')); |
| 467 | $tail = explode(\DIRECTORY_SEPARATOR, str_replace('/', \DIRECTORY_SEPARATOR, $file)); |
| 468 | $i = \count($tail) - 1; |
| 469 | $j = \count($real) - 1; |
| 470 | while (isset($tail[$i], $real[$j]) && $tail[$i] === $real[$j]) { |
| 471 | --$i; |
| 472 | --$j; |
| 473 | } |
| 474 | array_splice($tail, 0, $i + 1); |
| 475 | if (!$tail) { |
| 476 | return null; |
| 477 | } |
| 478 | $tail = \DIRECTORY_SEPARATOR . implode(\DIRECTORY_SEPARATOR, $tail); |
| 479 | $tailLen = \strlen($tail); |
| 480 | $real = $refl->getFileName(); |
| 481 | if (2 === self::$caseCheck) { |
| 482 | $real = $this->darwinRealpath($real); |
| 483 | } |
| 484 | if (0 === substr_compare($real, $tail, -$tailLen, $tailLen, \true) && 0 !== substr_compare($real, $tail, -$tailLen, $tailLen, \false)) { |
| 485 | return [substr($tail, -$tailLen + 1), substr($real, -$tailLen + 1), substr($real, 0, -$tailLen + 1)]; |
| 486 | } |
| 487 | return null; |
| 488 | } |
| 489 | /** |
| 490 | * `realpath` on MacOSX doesn't normalize the case of characters. |
| 491 | */ |
| 492 | private function darwinRealpath(string $real) : string |
| 493 | { |
| 494 | $i = 1 + strrpos($real, '/'); |
| 495 | $file = substr($real, $i); |
| 496 | $real = substr($real, 0, $i); |
| 497 | if (isset(self::$darwinCache[$real])) { |
| 498 | $kDir = $real; |
| 499 | } else { |
| 500 | $kDir = strtolower($real); |
| 501 | if (isset(self::$darwinCache[$kDir])) { |
| 502 | $real = self::$darwinCache[$kDir][0]; |
| 503 | } else { |
| 504 | $dir = getcwd(); |
| 505 | if (!@chdir($real)) { |
| 506 | return $real . $file; |
| 507 | } |
| 508 | $real = getcwd() . '/'; |
| 509 | chdir($dir); |
| 510 | $dir = $real; |
| 511 | $k = $kDir; |
| 512 | $i = \strlen($dir) - 1; |
| 513 | while (!isset(self::$darwinCache[$k])) { |
| 514 | self::$darwinCache[$k] = [$dir, []]; |
| 515 | self::$darwinCache[$dir] =& self::$darwinCache[$k]; |
| 516 | while ('/' !== $dir[--$i]) { |
| 517 | } |
| 518 | $k = substr($k, 0, ++$i); |
| 519 | $dir = substr($dir, 0, $i--); |
| 520 | } |
| 521 | } |
| 522 | } |
| 523 | $dirFiles = self::$darwinCache[$kDir][1]; |
| 524 | if (!isset($dirFiles[$file]) && ') : eval()\'d code' === substr($file, -17)) { |
| 525 | // Get the file name from "file_name.php(123) : eval()'d code" |
| 526 | $file = substr($file, 0, strrpos($file, '(', -17)); |
| 527 | } |
| 528 | if (isset($dirFiles[$file])) { |
| 529 | return $real . $dirFiles[$file]; |
| 530 | } |
| 531 | $kFile = strtolower($file); |
| 532 | if (!isset($dirFiles[$kFile])) { |
| 533 | foreach (scandir($real, 2) as $f) { |
| 534 | if ('.' !== $f[0]) { |
| 535 | $dirFiles[$f] = $f; |
| 536 | if ($f === $file) { |
| 537 | $kFile = $k = $file; |
| 538 | } elseif ($f !== ($k = strtolower($f))) { |
| 539 | $dirFiles[$k] = $f; |
| 540 | } |
| 541 | } |
| 542 | } |
| 543 | self::$darwinCache[$kDir][1] = $dirFiles; |
| 544 | } |
| 545 | return $real . $dirFiles[$kFile]; |
| 546 | } |
| 547 | /** |
| 548 | * `class_implements` includes interfaces from the parents so we have to manually exclude them. |
| 549 | * |
| 550 | * @return string[] |
| 551 | */ |
| 552 | private function getOwnInterfaces(string $class, ?string $parent) : array |
| 553 | { |
| 554 | $ownInterfaces = class_implements($class, \false); |
| 555 | if ($parent) { |
| 556 | foreach (class_implements($parent, \false) as $interface) { |
| 557 | unset($ownInterfaces[$interface]); |
| 558 | } |
| 559 | } |
| 560 | foreach ($ownInterfaces as $interface) { |
| 561 | foreach (class_implements($interface) as $interface) { |
| 562 | unset($ownInterfaces[$interface]); |
| 563 | } |
| 564 | } |
| 565 | return $ownInterfaces; |
| 566 | } |
| 567 | private function setReturnType(string $types, string $class, string $method, string $filename, ?string $parent, ?\ReflectionType $returnType = null) : void |
| 568 | { |
| 569 | if ('__construct' === $method) { |
| 570 | return; |
| 571 | } |
| 572 | if ('null' === $types) { |
| 573 | self::$returnTypes[$class][$method] = ['null', 'null', $class, $filename]; |
| 574 | return; |
| 575 | } |
| 576 | if ($nullable = 0 === strpos($types, 'null|')) { |
| 577 | $types = substr($types, 5); |
| 578 | } elseif ($nullable = '|null' === substr($types, -5)) { |
| 579 | $types = substr($types, 0, -5); |
| 580 | } |
| 581 | $arrayType = ['array' => 'array']; |
| 582 | $typesMap = []; |
| 583 | $glue = \false !== strpos($types, '&') ? '&' : '|'; |
| 584 | foreach (explode($glue, $types) as $t) { |
| 585 | $t = self::SPECIAL_RETURN_TYPES[strtolower($t)] ?? $t; |
| 586 | $typesMap[$this->normalizeType($t, $class, $parent, $returnType)][$t] = $t; |
| 587 | } |
| 588 | if (isset($typesMap['array'])) { |
| 589 | if (isset($typesMap['Traversable']) || isset($typesMap['\\Traversable'])) { |
| 590 | $typesMap['iterable'] = $arrayType !== $typesMap['array'] ? $typesMap['array'] : ['iterable']; |
| 591 | unset($typesMap['array'], $typesMap['Traversable'], $typesMap['\\Traversable']); |
| 592 | } elseif ($arrayType !== $typesMap['array'] && isset(self::$returnTypes[$class][$method]) && !$returnType) { |
| 593 | return; |
| 594 | } |
| 595 | } |
| 596 | if (isset($typesMap['array']) && isset($typesMap['iterable'])) { |
| 597 | if ($arrayType !== $typesMap['array']) { |
| 598 | $typesMap['iterable'] = $typesMap['array']; |
| 599 | } |
| 600 | unset($typesMap['array']); |
| 601 | } |
| 602 | $iterable = $object = \true; |
| 603 | foreach ($typesMap as $n => $t) { |
| 604 | if ('null' !== $n) { |
| 605 | $iterable = $iterable && (\in_array($n, ['array', 'iterable']) || \false !== strpos($n, 'Iterator')); |
| 606 | $object = $object && (\in_array($n, ['callable', 'object', '$this', 'static']) || !isset(self::SPECIAL_RETURN_TYPES[$n])); |
| 607 | } |
| 608 | } |
| 609 | $phpTypes = []; |
| 610 | $docTypes = []; |
| 611 | foreach ($typesMap as $n => $t) { |
| 612 | if ('null' === $n) { |
| 613 | $nullable = \true; |
| 614 | continue; |
| 615 | } |
| 616 | $docTypes[] = $t; |
| 617 | if ('mixed' === $n || 'void' === $n) { |
| 618 | $nullable = \false; |
| 619 | $phpTypes = ['' => $n]; |
| 620 | continue; |
| 621 | } |
| 622 | if ('resource' === $n) { |
| 623 | // there is no native type for "resource" |
| 624 | return; |
| 625 | } |
| 626 | if (!isset($phpTypes[''])) { |
| 627 | $phpTypes[] = $n; |
| 628 | } |
| 629 | } |
| 630 | $docTypes = array_merge([], ...$docTypes); |
| 631 | if (!$phpTypes) { |
| 632 | return; |
| 633 | } |
| 634 | if (1 < \count($phpTypes)) { |
| 635 | if ($iterable && '8.0' > $this->patchTypes['php']) { |
| 636 | $phpTypes = $docTypes = ['iterable']; |
| 637 | } elseif ($object && 'object' === $this->patchTypes['force']) { |
| 638 | $phpTypes = $docTypes = ['object']; |
| 639 | } elseif ('8.0' > $this->patchTypes['php']) { |
| 640 | // ignore multi-types return declarations |
| 641 | return; |
| 642 | } |
| 643 | } |
| 644 | $phpType = sprintf($nullable ? 1 < \count($phpTypes) ? '%s|null' : '?%s' : '%s', implode($glue, $phpTypes)); |
| 645 | $docType = sprintf($nullable ? '%s|null' : '%s', implode($glue, $docTypes)); |
| 646 | self::$returnTypes[$class][$method] = [$phpType, $docType, $class, $filename]; |
| 647 | } |
| 648 | private function normalizeType(string $type, string $class, ?string $parent, ?\ReflectionType $returnType) : string |
| 649 | { |
| 650 | if (isset(self::SPECIAL_RETURN_TYPES[$lcType = strtolower($type)])) { |
| 651 | if ('parent' === ($lcType = self::SPECIAL_RETURN_TYPES[$lcType])) { |
| 652 | $lcType = null !== $parent ? '\\' . $parent : 'parent'; |
| 653 | } elseif ('self' === $lcType) { |
| 654 | $lcType = '\\' . $class; |
| 655 | } |
| 656 | return $lcType; |
| 657 | } |
| 658 | // We could resolve "use" statements to return the FQDN |
| 659 | // but this would be too expensive for a runtime checker |
| 660 | if ('[]' !== substr($type, -2)) { |
| 661 | return $type; |
| 662 | } |
| 663 | if ($returnType instanceof \ReflectionNamedType) { |
| 664 | $type = $returnType->getName(); |
| 665 | if ('mixed' !== $type) { |
| 666 | return isset(self::SPECIAL_RETURN_TYPES[$type]) ? $type : '\\' . $type; |
| 667 | } |
| 668 | } |
| 669 | return 'array'; |
| 670 | } |
| 671 | /** |
| 672 | * Utility method to add #[ReturnTypeWillChange] where php triggers deprecations. |
| 673 | */ |
| 674 | private function patchReturnTypeWillChange(\ReflectionMethod $method) |
| 675 | { |
| 676 | if (\PHP_VERSION_ID >= 80000 && \count($method->getAttributes(\ReturnTypeWillChange::class))) { |
| 677 | return; |
| 678 | } |
| 679 | if (!is_file($file = $method->getFileName())) { |
| 680 | return; |
| 681 | } |
| 682 | $fileOffset = self::$fileOffsets[$file] ?? 0; |
| 683 | $code = file($file); |
| 684 | $startLine = $method->getStartLine() + $fileOffset - 2; |
| 685 | if (\false !== stripos($code[$startLine], 'ReturnTypeWillChange')) { |
| 686 | return; |
| 687 | } |
| 688 | $code[$startLine] .= " #[\\ReturnTypeWillChange]\n"; |
| 689 | self::$fileOffsets[$file] = 1 + $fileOffset; |
| 690 | file_put_contents($file, $code); |
| 691 | } |
| 692 | /** |
| 693 | * Utility method to add @return annotations to the Symfony code-base where it triggers self-deprecations. |
| 694 | */ |
| 695 | private function patchMethod(\ReflectionMethod $method, string $returnType, string $declaringFile, string $normalizedType) |
| 696 | { |
| 697 | static $patchedMethods = []; |
| 698 | static $useStatements = []; |
| 699 | if (!is_file($file = $method->getFileName()) || isset($patchedMethods[$file][$startLine = $method->getStartLine()])) { |
| 700 | return; |
| 701 | } |
| 702 | $patchedMethods[$file][$startLine] = \true; |
| 703 | $fileOffset = self::$fileOffsets[$file] ?? 0; |
| 704 | $startLine += $fileOffset - 2; |
| 705 | if ($nullable = '|null' === substr($returnType, -5)) { |
| 706 | $returnType = substr($returnType, 0, -5); |
| 707 | } |
| 708 | $glue = \false !== strpos($returnType, '&') ? '&' : '|'; |
| 709 | $returnType = explode($glue, $returnType); |
| 710 | $code = file($file); |
| 711 | foreach ($returnType as $i => $type) { |
| 712 | if (preg_match('/((?:\\[\\])+)$/', $type, $m)) { |
| 713 | $type = substr($type, 0, -\strlen($m[1])); |
| 714 | $format = '%s' . $m[1]; |
| 715 | } else { |
| 716 | $format = null; |
| 717 | } |
| 718 | if (isset(self::SPECIAL_RETURN_TYPES[$type]) || '\\' === $type[0] && !($p = strrpos($type, '\\', 1))) { |
| 719 | continue; |
| 720 | } |
| 721 | [$namespace, $useOffset, $useMap] = $useStatements[$file] ?? ($useStatements[$file] = self::getUseStatements($file)); |
| 722 | if ('\\' !== $type[0]) { |
| 723 | [$declaringNamespace, , $declaringUseMap] = $useStatements[$declaringFile] ?? ($useStatements[$declaringFile] = self::getUseStatements($declaringFile)); |
| 724 | $p = strpos($type, '\\', 1); |
| 725 | $alias = $p ? substr($type, 0, $p) : $type; |
| 726 | if (isset($declaringUseMap[$alias])) { |
| 727 | $type = '\\' . $declaringUseMap[$alias] . ($p ? substr($type, $p) : ''); |
| 728 | } else { |
| 729 | $type = '\\' . $declaringNamespace . $type; |
| 730 | } |
| 731 | $p = strrpos($type, '\\', 1); |
| 732 | } |
| 733 | $alias = substr($type, 1 + $p); |
| 734 | $type = substr($type, 1); |
| 735 | if (!isset($useMap[$alias]) && (class_exists($c = $namespace . $alias) || interface_exists($c) || trait_exists($c))) { |
| 736 | $useMap[$alias] = $c; |
| 737 | } |
| 738 | if (!isset($useMap[$alias])) { |
| 739 | $useStatements[$file][2][$alias] = $type; |
| 740 | $code[$useOffset] = "use {$type};\n" . $code[$useOffset]; |
| 741 | ++$fileOffset; |
| 742 | } elseif ($useMap[$alias] !== $type) { |
| 743 | $alias .= 'FIXME'; |
| 744 | $useStatements[$file][2][$alias] = $type; |
| 745 | $code[$useOffset] = "use {$type} as {$alias};\n" . $code[$useOffset]; |
| 746 | ++$fileOffset; |
| 747 | } |
| 748 | $returnType[$i] = null !== $format ? sprintf($format, $alias) : $alias; |
| 749 | } |
| 750 | if ('docblock' === $this->patchTypes['force'] || 'object' === $normalizedType && '7.1' === $this->patchTypes['php']) { |
| 751 | $returnType = implode($glue, $returnType) . ($nullable ? '|null' : ''); |
| 752 | if (\false !== strpos($code[$startLine], '#[')) { |
| 753 | --$startLine; |
| 754 | } |
| 755 | if ($method->getDocComment()) { |
| 756 | $code[$startLine] = " * @return {$returnType}\n" . $code[$startLine]; |
| 757 | } else { |
| 758 | $code[$startLine] .= <<<EOTXT |
| 759 | /** |
| 760 | * @return {$returnType} |
| 761 | */ |
| 762 | |
| 763 | EOTXT; |
| 764 | } |
| 765 | $fileOffset += substr_count($code[$startLine], "\n") - 1; |
| 766 | } |
| 767 | self::$fileOffsets[$file] = $fileOffset; |
| 768 | file_put_contents($file, $code); |
| 769 | $this->fixReturnStatements($method, $normalizedType); |
| 770 | } |
| 771 | private static function getUseStatements(string $file) : array |
| 772 | { |
| 773 | $namespace = ''; |
| 774 | $useMap = []; |
| 775 | $useOffset = 0; |
| 776 | if (!is_file($file)) { |
| 777 | return [$namespace, $useOffset, $useMap]; |
| 778 | } |
| 779 | $file = file($file); |
| 780 | for ($i = 0; $i < \count($file); ++$i) { |
| 781 | if (preg_match('/^(class|interface|trait|abstract) /', $file[$i])) { |
| 782 | break; |
| 783 | } |
| 784 | if (0 === strpos($file[$i], 'namespace ')) { |
| 785 | $namespace = substr($file[$i], \strlen('namespace '), -2) . '\\'; |
| 786 | $useOffset = $i + 2; |
| 787 | } |
| 788 | if (0 === strpos($file[$i], 'use ')) { |
| 789 | $useOffset = $i; |
| 790 | for (; 0 === strpos($file[$i], 'use '); ++$i) { |
| 791 | $u = explode(' as ', substr($file[$i], 4, -2), 2); |
| 792 | if (1 === \count($u)) { |
| 793 | $p = strrpos($u[0], '\\'); |
| 794 | $useMap[substr($u[0], \false !== $p ? 1 + $p : 0)] = $u[0]; |
| 795 | } else { |
| 796 | $useMap[$u[1]] = $u[0]; |
| 797 | } |
| 798 | } |
| 799 | break; |
| 800 | } |
| 801 | } |
| 802 | return [$namespace, $useOffset, $useMap]; |
| 803 | } |
| 804 | private function fixReturnStatements(\ReflectionMethod $method, string $returnType) |
| 805 | { |
| 806 | if ('docblock' !== $this->patchTypes['force']) { |
| 807 | if ('7.1' === $this->patchTypes['php'] && 'object' === ltrim($returnType, '?')) { |
| 808 | return; |
| 809 | } |
| 810 | if ('7.4' > $this->patchTypes['php'] && $method->hasReturnType()) { |
| 811 | return; |
| 812 | } |
| 813 | if ('8.0' > $this->patchTypes['php'] && (\false !== strpos($returnType, '|') || \in_array($returnType, ['mixed', 'static'], \true))) { |
| 814 | return; |
| 815 | } |
| 816 | if ('8.1' > $this->patchTypes['php'] && \false !== strpos($returnType, '&')) { |
| 817 | return; |
| 818 | } |
| 819 | } |
| 820 | if (!is_file($file = $method->getFileName())) { |
| 821 | return; |
| 822 | } |
| 823 | $fixedCode = $code = file($file); |
| 824 | $i = (self::$fileOffsets[$file] ?? 0) + $method->getStartLine(); |
| 825 | if ('?' !== $returnType && 'docblock' !== $this->patchTypes['force']) { |
| 826 | $fixedCode[$i - 1] = preg_replace('/\\)(?::[^;\\n]++)?(;?\\n)/', "): {$returnType}\\1", $code[$i - 1]); |
| 827 | } |
| 828 | $end = $method->isGenerator() ? $i : $method->getEndLine(); |
| 829 | $inClosure = \false; |
| 830 | $braces = 0; |
| 831 | for (; $i < $end; ++$i) { |
| 832 | if (!$inClosure) { |
| 833 | $inClosure = \false !== strpos($code[$i], 'function ('); |
| 834 | } |
| 835 | if ($inClosure) { |
| 836 | $braces += substr_count($code[$i], '{') - substr_count($code[$i], '}'); |
| 837 | $inClosure = $braces > 0; |
| 838 | continue; |
| 839 | } |
| 840 | if ('void' === $returnType) { |
| 841 | $fixedCode[$i] = str_replace(' return null;', ' return;', $code[$i]); |
| 842 | } elseif ('mixed' === $returnType || '?' === $returnType[0]) { |
| 843 | $fixedCode[$i] = str_replace(' return;', ' return null;', $code[$i]); |
| 844 | } else { |
| 845 | $fixedCode[$i] = str_replace(' return;', " return {$returnType}!?;", $code[$i]); |
| 846 | } |
| 847 | } |
| 848 | if ($fixedCode !== $code) { |
| 849 | file_put_contents($file, $fixedCode); |
| 850 | } |
| 851 | } |
| 852 | /** |
| 853 | * @param \ReflectionClass|\ReflectionMethod|\ReflectionProperty $reflector |
| 854 | */ |
| 855 | private function parsePhpDoc(\Reflector $reflector) : array |
| 856 | { |
| 857 | if (!($doc = $reflector->getDocComment())) { |
| 858 | return []; |
| 859 | } |
| 860 | $tagName = ''; |
| 861 | $tagContent = ''; |
| 862 | $tags = []; |
| 863 | foreach (explode("\n", substr($doc, 3, -2)) as $line) { |
| 864 | $line = ltrim($line); |
| 865 | $line = ltrim($line, '*'); |
| 866 | if ('' === ($line = trim($line))) { |
| 867 | if ('' !== $tagName) { |
| 868 | $tags[$tagName][] = $tagContent; |
| 869 | } |
| 870 | $tagName = $tagContent = ''; |
| 871 | continue; |
| 872 | } |
| 873 | if ('@' === $line[0]) { |
| 874 | if ('' !== $tagName) { |
| 875 | $tags[$tagName][] = $tagContent; |
| 876 | $tagContent = ''; |
| 877 | } |
| 878 | if (preg_match('{^@([-a-zA-Z0-9_:]++)(\\s|$)}', $line, $m)) { |
| 879 | $tagName = $m[1]; |
| 880 | $tagContent = str_replace("\t", ' ', ltrim(substr($line, 2 + \strlen($tagName)))); |
| 881 | } else { |
| 882 | $tagName = ''; |
| 883 | } |
| 884 | } elseif ('' !== $tagName) { |
| 885 | $tagContent .= ' ' . str_replace("\t", ' ', $line); |
| 886 | } |
| 887 | } |
| 888 | if ('' !== $tagName) { |
| 889 | $tags[$tagName][] = $tagContent; |
| 890 | } |
| 891 | foreach ($tags['method'] ?? [] as $i => $method) { |
| 892 | unset($tags['method'][$i]); |
| 893 | $parts = preg_split('{(\\s++|\\((?:[^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<(?:[^<>]*+|(?R))*>|\\{(?:[^{}]*+|(?R))*\\})}', $method, -1, \PREG_SPLIT_DELIM_CAPTURE); |
| 894 | $returnType = ''; |
| 895 | $static = 'static' === $parts[0]; |
| 896 | for ($i = $static ? 2 : 0; null !== ($p = $parts[$i] ?? null); $i += 2) { |
| 897 | if (\in_array($p, ['', '|', '&', 'callable'], \true) || \in_array(substr($returnType, -1), ['|', '&'], \true)) { |
| 898 | $returnType .= trim($parts[$i - 1] ?? '') . $p; |
| 899 | continue; |
| 900 | } |
| 901 | $signature = '(' === ($parts[$i + 1][0] ?? '(') ? $parts[$i + 1] ?? '()' : null; |
| 902 | if (null === $signature && '' === $returnType) { |
| 903 | $returnType = $p; |
| 904 | continue; |
| 905 | } |
| 906 | if ($static && 2 === $i) { |
| 907 | $static = \false; |
| 908 | $returnType = 'static'; |
| 909 | } |
| 910 | if (\in_array($description = trim(implode('', \array_slice($parts, 2 + $i))), ['', '.'], \true)) { |
| 911 | $description = null; |
| 912 | } elseif (!preg_match('/[.!]$/', $description)) { |
| 913 | $description .= '.'; |
| 914 | } |
| 915 | $tags['method'][$p] = [$static, $returnType, $signature ?? '()', $description]; |
| 916 | break; |
| 917 | } |
| 918 | } |
| 919 | foreach ($tags['param'] ?? [] as $i => $param) { |
| 920 | unset($tags['param'][$i]); |
| 921 | if (\strlen($param) !== strcspn($param, '<{(')) { |
| 922 | $param = preg_replace('{\\(([^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\\{([^{}]*+|(?R))*\\}}', '', $param); |
| 923 | } |
| 924 | if (\false === ($i = strpos($param, '$'))) { |
| 925 | continue; |
| 926 | } |
| 927 | $type = 0 === $i ? '' : rtrim(substr($param, 0, $i), ' &'); |
| 928 | $param = substr($param, 1 + $i, (strpos($param, ' ', $i) ?: 1 + $i + \strlen($param)) - $i - 1); |
| 929 | $tags['param'][$param] = $type; |
| 930 | } |
| 931 | foreach (['var', 'return'] as $k) { |
| 932 | if (null === ($v = $tags[$k][0] ?? null)) { |
| 933 | continue; |
| 934 | } |
| 935 | if (\strlen($v) !== strcspn($v, '<{(')) { |
| 936 | $v = preg_replace('{\\(([^()]*+|(?R))*\\)(?: *: *[^ ]++)?|<([^<>]*+|(?R))*>|\\{([^{}]*+|(?R))*\\}}', '', $v); |
| 937 | } |
| 938 | $tags[$k] = substr($v, 0, strpos($v, ' ') ?: \strlen($v)) ?: null; |
| 939 | } |
| 940 | return $tags; |
| 941 | } |
| 942 | } |
| 943 |