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