1#!/usr/bin/env php 2<?php declare(strict_types=1); 3 4use PhpParser\Comment\Doc as DocComment; 5use PhpParser\ConstExprEvaluator; 6use PhpParser\Node; 7use PhpParser\Node\Expr; 8use PhpParser\Node\Name; 9use PhpParser\Node\Stmt; 10use PhpParser\Node\Stmt\Class_; 11use PhpParser\Node\Stmt\Enum_; 12use PhpParser\Node\Stmt\Interface_; 13use PhpParser\Node\Stmt\Trait_; 14use PhpParser\PrettyPrinter\Standard; 15use PhpParser\PrettyPrinterAbstract; 16 17error_reporting(E_ALL); 18ini_set("precision", "-1"); 19 20const PHP_70_VERSION_ID = 70000; 21const PHP_80_VERSION_ID = 80000; 22const PHP_81_VERSION_ID = 80100; 23const PHP_82_VERSION_ID = 80200; 24const ALL_PHP_VERSION_IDS = [PHP_70_VERSION_ID, PHP_80_VERSION_ID, PHP_81_VERSION_ID, PHP_82_VERSION_ID]; 25 26/** 27 * @return FileInfo[] 28 */ 29function processDirectory(string $dir, Context $context): array { 30 $pathNames = []; 31 $it = new RecursiveIteratorIterator( 32 new RecursiveDirectoryIterator($dir), 33 RecursiveIteratorIterator::LEAVES_ONLY 34 ); 35 foreach ($it as $file) { 36 $pathName = $file->getPathName(); 37 if (preg_match('/\.stub\.php$/', $pathName)) { 38 $pathNames[] = $pathName; 39 } 40 } 41 42 // Make sure stub files are processed in a predictable, system-independent order. 43 sort($pathNames); 44 45 $fileInfos = []; 46 foreach ($pathNames as $pathName) { 47 $fileInfo = processStubFile($pathName, $context); 48 if ($fileInfo) { 49 $fileInfos[] = $fileInfo; 50 } 51 } 52 return $fileInfos; 53} 54 55function processStubFile(string $stubFile, Context $context, bool $includeOnly = false): ?FileInfo { 56 try { 57 if (!file_exists($stubFile)) { 58 throw new Exception("File $stubFile does not exist"); 59 } 60 61 if (!$includeOnly) { 62 $stubFilenameWithoutExtension = str_replace(".stub.php", "", $stubFile); 63 $arginfoFile = "{$stubFilenameWithoutExtension}_arginfo.h"; 64 $legacyFile = "{$stubFilenameWithoutExtension}_legacy_arginfo.h"; 65 66 $stubCode = file_get_contents($stubFile); 67 $stubHash = computeStubHash($stubCode); 68 $oldStubHash = extractStubHash($arginfoFile); 69 if ($stubHash === $oldStubHash && !$context->forceParse) { 70 /* Stub file did not change, do not regenerate. */ 71 return null; 72 } 73 } 74 75 if (!$fileInfo = $context->parsedFiles[$stubFile] ?? null) { 76 initPhpParser(); 77 $fileInfo = parseStubFile($stubCode ?? file_get_contents($stubFile)); 78 $context->parsedFiles[$stubFile] = $fileInfo; 79 80 foreach ($fileInfo->dependencies as $dependency) { 81 // TODO add header search path for extensions? 82 $prefixes = [dirname($stubFile) . "/", dirname(__DIR__) . "/"]; 83 foreach ($prefixes as $prefix) { 84 $depFile = $prefix . $dependency; 85 if (file_exists($depFile)) { 86 break; 87 } 88 $depFile = null; 89 } 90 if (!$depFile) { 91 throw new Exception("File $stubFile includes a file $dependency which does not exist"); 92 } 93 processStubFile($depFile, $context, true); 94 } 95 96 $constInfos = $fileInfo->getAllConstInfos(); 97 $context->allConstInfos = array_merge($context->allConstInfos, $constInfos); 98 } 99 100 if ($includeOnly) { 101 return $fileInfo; 102 } 103 104 $arginfoCode = generateArgInfoCode( 105 basename($stubFilenameWithoutExtension), 106 $fileInfo, 107 $context->allConstInfos, 108 $stubHash 109 ); 110 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { 111 echo "Saved $arginfoFile\n"; 112 } 113 114 if ($fileInfo->generateLegacyArginfoForPhpVersionId !== null && $fileInfo->generateLegacyArginfoForPhpVersionId < PHP_80_VERSION_ID) { 115 $legacyFileInfo = clone $fileInfo; 116 117 foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) { 118 $funcInfo->discardInfoForOldPhpVersions(); 119 } 120 foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) { 121 $constInfo->discardInfoForOldPhpVersions(); 122 } 123 foreach ($legacyFileInfo->getAllPropertyInfos() as $propertyInfo) { 124 $propertyInfo->discardInfoForOldPhpVersions(); 125 } 126 127 $arginfoCode = generateArgInfoCode( 128 basename($stubFilenameWithoutExtension), 129 $legacyFileInfo, 130 $context->allConstInfos, 131 $stubHash 132 ); 133 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { 134 echo "Saved $legacyFile\n"; 135 } 136 } 137 138 return $fileInfo; 139 } catch (Exception $e) { 140 echo "In $stubFile:\n{$e->getMessage()}\n"; 141 exit(1); 142 } 143} 144 145function computeStubHash(string $stubCode): string { 146 return sha1(str_replace("\r\n", "\n", $stubCode)); 147} 148 149function extractStubHash(string $arginfoFile): ?string { 150 if (!file_exists($arginfoFile)) { 151 return null; 152 } 153 154 $arginfoCode = file_get_contents($arginfoFile); 155 if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) { 156 return null; 157 } 158 159 return $matches[1]; 160} 161 162class Context { 163 public bool $forceParse = false; 164 public bool $forceRegeneration = false; 165 /** @var iterable<ConstInfo> */ 166 public iterable $allConstInfos = []; 167 /** @var FileInfo[] */ 168 public array $parsedFiles = []; 169} 170 171class ArrayType extends SimpleType { 172 public Type $keyType; 173 public Type $valueType; 174 175 public static function createGenericArray(): self 176 { 177 return new ArrayType(Type::fromString("int|string"), Type::fromString("mixed|ref")); 178 } 179 180 public function __construct(Type $keyType, Type $valueType) 181 { 182 parent::__construct("array", true); 183 184 $this->keyType = $keyType; 185 $this->valueType = $valueType; 186 } 187 188 public function toOptimizerTypeMask(): string { 189 $typeMasks = [ 190 parent::toOptimizerTypeMask(), 191 $this->keyType->toOptimizerTypeMaskForArrayKey(), 192 $this->valueType->toOptimizerTypeMaskForArrayValue(), 193 ]; 194 195 return implode("|", $typeMasks); 196 } 197 198 public function equals(SimpleType $other): bool { 199 if (!parent::equals($other)) { 200 return false; 201 } 202 203 assert(get_class($other) === self::class); 204 205 return Type::equals($this->keyType, $other->keyType) && 206 Type::equals($this->valueType, $other->valueType); 207 } 208} 209 210class SimpleType { 211 public string $name; 212 public bool $isBuiltin; 213 214 public static function fromNode(Node $node): SimpleType { 215 if ($node instanceof Node\Name) { 216 if ($node->toLowerString() === 'static') { 217 // PHP internally considers "static" a builtin type. 218 return new SimpleType($node->toLowerString(), true); 219 } 220 221 if ($node->toLowerString() === 'true') { 222 // TODO PHP-Parser doesn't yet recognize true as a stand-alone built-in type 223 return new SimpleType($node->toLowerString(), true); 224 } 225 226 if ($node->toLowerString() === 'self') { 227 throw new Exception('The exact class name must be used instead of "self"'); 228 } 229 230 assert($node->isFullyQualified()); 231 return new SimpleType($node->toString(), false); 232 } 233 234 if ($node instanceof Node\Identifier) { 235 if ($node->toLowerString() === 'array') { 236 return ArrayType::createGenericArray(); 237 } 238 239 return new SimpleType($node->toLowerString(), true); 240 } 241 242 throw new Exception("Unexpected node type"); 243 } 244 245 public static function fromString(string $typeString): SimpleType 246 { 247 switch (strtolower($typeString)) { 248 case "void": 249 case "null": 250 case "false": 251 case "true": 252 case "bool": 253 case "int": 254 case "float": 255 case "string": 256 case "callable": 257 case "object": 258 case "resource": 259 case "mixed": 260 case "static": 261 case "never": 262 case "ref": 263 return new SimpleType(strtolower($typeString), true); 264 case "array": 265 return ArrayType::createGenericArray(); 266 case "self": 267 throw new Exception('The exact class name must be used instead of "self"'); 268 case "iterable": 269 throw new Exception('This should not happen'); 270 } 271 272 $matches = []; 273 $isArray = preg_match("/(.*)\s*\[\s*\]/", $typeString, $matches); 274 if ($isArray) { 275 return new ArrayType(Type::fromString("int"), Type::fromString($matches[1])); 276 } 277 278 $matches = []; 279 $isArray = preg_match("/array\s*<\s*([A-Za-z0-9_-|]+)?(\s*,\s*)?([A-Za-z0-9_-|]+)?\s*>/i", $typeString, $matches); 280 if ($isArray) { 281 if (empty($matches[1]) || empty($matches[3])) { 282 throw new Exception("array<> type hint must have both a key and a value"); 283 } 284 285 return new ArrayType(Type::fromString($matches[1]), Type::fromString($matches[3])); 286 } 287 288 return new SimpleType($typeString, false); 289 } 290 291 /** 292 * @param mixed $value 293 */ 294 public static function fromValue($value): SimpleType 295 { 296 switch (gettype($value)) { 297 case "NULL": 298 return SimpleType::null(); 299 case "boolean": 300 return SimpleType::bool(); 301 case "integer": 302 return SimpleType::int(); 303 case "double": 304 return SimpleType::float(); 305 case "string": 306 return SimpleType::string(); 307 case "array": 308 return SimpleType::array(); 309 case "object": 310 return SimpleType::object(); 311 default: 312 throw new Exception("Type \"" . gettype($value) . "\" cannot be inferred based on value"); 313 } 314 } 315 316 public static function null(): SimpleType 317 { 318 return new SimpleType("null", true); 319 } 320 321 public static function bool(): SimpleType 322 { 323 return new SimpleType("bool", true); 324 } 325 326 public static function int(): SimpleType 327 { 328 return new SimpleType("int", true); 329 } 330 331 public static function float(): SimpleType 332 { 333 return new SimpleType("float", true); 334 } 335 336 public static function string(): SimpleType 337 { 338 return new SimpleType("string", true); 339 } 340 341 public static function array(): SimpleType 342 { 343 return new SimpleType("array", true); 344 } 345 346 public static function object(): SimpleType 347 { 348 return new SimpleType("object", true); 349 } 350 351 public static function void(): SimpleType 352 { 353 return new SimpleType("void", true); 354 } 355 356 protected function __construct(string $name, bool $isBuiltin) { 357 $this->name = $name; 358 $this->isBuiltin = $isBuiltin; 359 } 360 361 public function isScalar(): bool { 362 return $this->isBuiltin && in_array($this->name, ["null", "false", "true", "bool", "int", "float"], true); 363 } 364 365 public function isNull(): bool { 366 return $this->isBuiltin && $this->name === 'null'; 367 } 368 369 public function isBool(): bool { 370 return $this->isBuiltin && $this->name === 'bool'; 371 } 372 373 public function isInt(): bool { 374 return $this->isBuiltin && $this->name === 'int'; 375 } 376 377 public function isFloat(): bool { 378 return $this->isBuiltin && $this->name === 'float'; 379 } 380 381 public function isString(): bool { 382 return $this->isBuiltin && $this->name === 'string'; 383 } 384 385 public function isArray(): bool { 386 return $this->isBuiltin && $this->name === 'array'; 387 } 388 389 public function toTypeCode(): string { 390 assert($this->isBuiltin); 391 switch ($this->name) { 392 case "bool": 393 return "_IS_BOOL"; 394 case "int": 395 return "IS_LONG"; 396 case "float": 397 return "IS_DOUBLE"; 398 case "string": 399 return "IS_STRING"; 400 case "array": 401 return "IS_ARRAY"; 402 case "object": 403 return "IS_OBJECT"; 404 case "void": 405 return "IS_VOID"; 406 case "callable": 407 return "IS_CALLABLE"; 408 case "mixed": 409 return "IS_MIXED"; 410 case "static": 411 return "IS_STATIC"; 412 case "never": 413 return "IS_NEVER"; 414 case "null": 415 return "IS_NULL"; 416 case "false": 417 return "IS_FALSE"; 418 case "true": 419 return "IS_TRUE"; 420 default: 421 throw new Exception("Not implemented: $this->name"); 422 } 423 } 424 425 public function toTypeMask(): string { 426 assert($this->isBuiltin); 427 428 switch ($this->name) { 429 case "null": 430 return "MAY_BE_NULL"; 431 case "false": 432 return "MAY_BE_FALSE"; 433 case "true": 434 return "MAY_BE_TRUE"; 435 case "bool": 436 return "MAY_BE_BOOL"; 437 case "int": 438 return "MAY_BE_LONG"; 439 case "float": 440 return "MAY_BE_DOUBLE"; 441 case "string": 442 return "MAY_BE_STRING"; 443 case "array": 444 return "MAY_BE_ARRAY"; 445 case "object": 446 return "MAY_BE_OBJECT"; 447 case "callable": 448 return "MAY_BE_CALLABLE"; 449 case "mixed": 450 return "MAY_BE_ANY"; 451 case "void": 452 return "MAY_BE_VOID"; 453 case "static": 454 return "MAY_BE_STATIC"; 455 case "never": 456 return "MAY_BE_NEVER"; 457 default: 458 throw new Exception("Not implemented: $this->name"); 459 } 460 } 461 462 public function toOptimizerTypeMaskForArrayKey(): string { 463 assert($this->isBuiltin); 464 465 switch ($this->name) { 466 case "int": 467 return "MAY_BE_ARRAY_KEY_LONG"; 468 case "string": 469 return "MAY_BE_ARRAY_KEY_STRING"; 470 default: 471 throw new Exception("Type $this->name cannot be an array key"); 472 } 473 } 474 475 public function toOptimizerTypeMaskForArrayValue(): string { 476 if (!$this->isBuiltin) { 477 return "MAY_BE_ARRAY_OF_OBJECT"; 478 } 479 480 switch ($this->name) { 481 case "null": 482 return "MAY_BE_ARRAY_OF_NULL"; 483 case "false": 484 return "MAY_BE_ARRAY_OF_FALSE"; 485 case "true": 486 return "MAY_BE_ARRAY_OF_TRUE"; 487 case "bool": 488 return "MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE"; 489 case "int": 490 return "MAY_BE_ARRAY_OF_LONG"; 491 case "float": 492 return "MAY_BE_ARRAY_OF_DOUBLE"; 493 case "string": 494 return "MAY_BE_ARRAY_OF_STRING"; 495 case "array": 496 return "MAY_BE_ARRAY_OF_ARRAY"; 497 case "object": 498 return "MAY_BE_ARRAY_OF_OBJECT"; 499 case "resource": 500 return "MAY_BE_ARRAY_OF_RESOURCE"; 501 case "mixed": 502 return "MAY_BE_ARRAY_OF_ANY"; 503 case "ref": 504 return "MAY_BE_ARRAY_OF_REF"; 505 default: 506 throw new Exception("Type $this->name cannot be an array value"); 507 } 508 } 509 510 public function toOptimizerTypeMask(): string { 511 if (!$this->isBuiltin) { 512 return "MAY_BE_OBJECT"; 513 } 514 515 switch ($this->name) { 516 case "resource": 517 return "MAY_BE_RESOURCE"; 518 case "callable": 519 return "MAY_BE_STRING|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_STRING|MAY_BE_ARRAY_OF_OBJECT|MAY_BE_OBJECT"; 520 case "iterable": 521 return "MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_OBJECT"; 522 case "mixed": 523 return "MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY"; 524 } 525 526 return $this->toTypeMask(); 527 } 528 529 public function toEscapedName(): string { 530 // Escape backslashes, and also encode \u, \U, and \N to avoid compilation errors in generated macros 531 return str_replace( 532 ['\\', '\\u', '\\U', '\\N'], 533 ['\\\\', '\\\\165', '\\\\125', '\\\\116'], 534 $this->name 535 ); 536 } 537 538 public function toVarEscapedName(): string { 539 return str_replace('\\', '_', $this->name); 540 } 541 542 public function equals(SimpleType $other): bool { 543 return $this->name === $other->name && $this->isBuiltin === $other->isBuiltin; 544 } 545} 546 547class Type { 548 /** @var SimpleType[] */ 549 public array $types; 550 public bool $isIntersection; 551 552 public static function fromNode(Node $node): Type { 553 if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) { 554 $nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types); 555 $types = []; 556 foreach ($nestedTypeObjects as $typeObject) { 557 array_push($types, ...$typeObject->types); 558 } 559 return new Type($types, ($node instanceof Node\IntersectionType)); 560 } 561 562 if ($node instanceof Node\NullableType) { 563 return new Type( 564 [ 565 ...Type::fromNode($node->type)->types, 566 SimpleType::null(), 567 ], 568 false 569 ); 570 } 571 572 if ($node instanceof Node\Identifier && $node->toLowerString() === "iterable") { 573 return new Type( 574 [ 575 SimpleType::fromString("Traversable"), 576 ArrayType::createGenericArray(), 577 ], 578 false 579 ); 580 } 581 582 return new Type([SimpleType::fromNode($node)], false); 583 } 584 585 public static function fromString(string $typeString): self { 586 $typeString .= "|"; 587 $simpleTypes = []; 588 $simpleTypeOffset = 0; 589 $inArray = false; 590 $isIntersection = false; 591 592 $typeStringLength = strlen($typeString); 593 for ($i = 0; $i < $typeStringLength; $i++) { 594 $char = $typeString[$i]; 595 596 if ($char === "<") { 597 $inArray = true; 598 continue; 599 } 600 601 if ($char === ">") { 602 $inArray = false; 603 continue; 604 } 605 606 if ($inArray) { 607 continue; 608 } 609 610 if ($char === "|" || $char === "&") { 611 $isIntersection = ($char === "&"); 612 $simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset)); 613 614 $simpleTypes[] = SimpleType::fromString($simpleTypeName); 615 616 $simpleTypeOffset = $i + 1; 617 } 618 } 619 620 return new Type($simpleTypes, $isIntersection); 621 } 622 623 /** 624 * @param SimpleType[] $types 625 */ 626 private function __construct(array $types, bool $isIntersection) { 627 $this->types = $types; 628 $this->isIntersection = $isIntersection; 629 } 630 631 public function isScalar(): bool { 632 foreach ($this->types as $type) { 633 if (!$type->isScalar()) { 634 return false; 635 } 636 } 637 638 return true; 639 } 640 641 public function isNullable(): bool { 642 foreach ($this->types as $type) { 643 if ($type->isNull()) { 644 return true; 645 } 646 } 647 648 return false; 649 } 650 651 public function getWithoutNull(): Type { 652 return new Type( 653 array_filter( 654 $this->types, 655 function(SimpleType $type) { 656 return !$type->isNull(); 657 } 658 ), 659 false 660 ); 661 } 662 663 public function tryToSimpleType(): ?SimpleType { 664 $withoutNull = $this->getWithoutNull(); 665 /* type has only null */ 666 if (count($withoutNull->types) === 0) { 667 return $this->types[0]; 668 } 669 if (count($withoutNull->types) === 1) { 670 return $withoutNull->types[0]; 671 } 672 return null; 673 } 674 675 public function toArginfoType(): ArginfoType { 676 $classTypes = []; 677 $builtinTypes = []; 678 foreach ($this->types as $type) { 679 if ($type->isBuiltin) { 680 $builtinTypes[] = $type; 681 } else { 682 $classTypes[] = $type; 683 } 684 } 685 return new ArginfoType($classTypes, $builtinTypes); 686 } 687 688 public function toOptimizerTypeMask(): string { 689 $optimizerTypes = []; 690 691 foreach ($this->types as $type) { 692 // TODO Support for toOptimizerMask for intersection 693 $optimizerTypes[] = $type->toOptimizerTypeMask(); 694 } 695 696 return implode("|", $optimizerTypes); 697 } 698 699 public function toOptimizerTypeMaskForArrayKey(): string { 700 $typeMasks = []; 701 702 foreach ($this->types as $type) { 703 $typeMasks[] = $type->toOptimizerTypeMaskForArrayKey(); 704 } 705 706 return implode("|", $typeMasks); 707 } 708 709 public function toOptimizerTypeMaskForArrayValue(): string { 710 $typeMasks = []; 711 712 foreach ($this->types as $type) { 713 $typeMasks[] = $type->toOptimizerTypeMaskForArrayValue(); 714 } 715 716 return implode("|", $typeMasks); 717 } 718 719 public function getTypeForDoc(DOMDocument $doc): DOMElement { 720 if (count($this->types) > 1) { 721 $typeSort = $this->isIntersection ? "intersection" : "union"; 722 $typeElement = $doc->createElement('type'); 723 $typeElement->setAttribute("class", $typeSort); 724 725 foreach ($this->types as $type) { 726 $unionTypeElement = $doc->createElement('type', $type->name); 727 $typeElement->appendChild($unionTypeElement); 728 } 729 } else { 730 $type = $this->types[0]; 731 if ($type->isBuiltin && strtolower($type->name) === "true") { 732 $name = "bool"; 733 } else { 734 $name = $type->name; 735 } 736 737 $typeElement = $doc->createElement('type', $name); 738 } 739 740 return $typeElement; 741 } 742 743 public static function equals(?Type $a, ?Type $b): bool { 744 if ($a === null || $b === null) { 745 return $a === $b; 746 } 747 748 if (count($a->types) !== count($b->types)) { 749 return false; 750 } 751 752 for ($i = 0; $i < count($a->types); $i++) { 753 if (!$a->types[$i]->equals($b->types[$i])) { 754 return false; 755 } 756 } 757 758 return true; 759 } 760 761 public function __toString() { 762 if ($this->types === null) { 763 return 'mixed'; 764 } 765 766 $char = $this->isIntersection ? '&' : '|'; 767 return implode($char, array_map( 768 function ($type) { return $type->name; }, 769 $this->types) 770 ); 771 } 772} 773 774class ArginfoType { 775 /** @var SimpleType[] $classTypes */ 776 public array $classTypes; 777 /** @var SimpleType[] $builtinTypes */ 778 private array $builtinTypes; 779 780 /** 781 * @param SimpleType[] $classTypes 782 * @param SimpleType[] $builtinTypes 783 */ 784 public function __construct(array $classTypes, array $builtinTypes) { 785 $this->classTypes = $classTypes; 786 $this->builtinTypes = $builtinTypes; 787 } 788 789 public function hasClassType(): bool { 790 return !empty($this->classTypes); 791 } 792 793 public function toClassTypeString(): string { 794 return implode('|', array_map(function(SimpleType $type) { 795 return $type->toEscapedName(); 796 }, $this->classTypes)); 797 } 798 799 public function toTypeMask(): string { 800 if (empty($this->builtinTypes)) { 801 return '0'; 802 } 803 return implode('|', array_map(function(SimpleType $type) { 804 return $type->toTypeMask(); 805 }, $this->builtinTypes)); 806 } 807} 808 809class ArgInfo { 810 const SEND_BY_VAL = 0; 811 const SEND_BY_REF = 1; 812 const SEND_PREFER_REF = 2; 813 814 public string $name; 815 public int $sendBy; 816 public bool $isVariadic; 817 public ?Type $type; 818 public ?Type $phpDocType; 819 public ?string $defaultValue; 820 /** @var AttributeInfo[] */ 821 public array $attributes; 822 823 /** 824 * @param AttributeInfo[] $attributes 825 */ 826 public function __construct( 827 string $name, 828 int $sendBy, 829 bool $isVariadic, 830 ?Type $type, 831 ?Type $phpDocType, 832 ?string $defaultValue, 833 array $attributes 834 ) { 835 $this->name = $name; 836 $this->sendBy = $sendBy; 837 $this->isVariadic = $isVariadic; 838 $this->setTypes($type, $phpDocType); 839 $this->defaultValue = $defaultValue; 840 $this->attributes = $attributes; 841 } 842 843 public function equals(ArgInfo $other): bool { 844 return $this->name === $other->name 845 && $this->sendBy === $other->sendBy 846 && $this->isVariadic === $other->isVariadic 847 && Type::equals($this->type, $other->type) 848 && $this->defaultValue === $other->defaultValue; 849 } 850 851 public function getSendByString(): string { 852 switch ($this->sendBy) { 853 case self::SEND_BY_VAL: 854 return "0"; 855 case self::SEND_BY_REF: 856 return "1"; 857 case self::SEND_PREFER_REF: 858 return "ZEND_SEND_PREFER_REF"; 859 } 860 throw new Exception("Invalid sendBy value"); 861 } 862 863 public function getMethodSynopsisType(): Type { 864 if ($this->type) { 865 return $this->type; 866 } 867 868 if ($this->phpDocType) { 869 return $this->phpDocType; 870 } 871 872 throw new Exception("A parameter must have a type"); 873 } 874 875 public function hasProperDefaultValue(): bool { 876 return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN"; 877 } 878 879 public function getDefaultValueAsArginfoString(): string { 880 if ($this->hasProperDefaultValue()) { 881 return '"' . addslashes($this->defaultValue) . '"'; 882 } 883 884 return "NULL"; 885 } 886 887 public function getDefaultValueAsMethodSynopsisString(): ?string { 888 if ($this->defaultValue === null) { 889 return null; 890 } 891 892 switch ($this->defaultValue) { 893 case 'UNKNOWN': 894 return null; 895 case 'false': 896 case 'true': 897 case 'null': 898 return "&{$this->defaultValue};"; 899 } 900 901 return $this->defaultValue; 902 } 903 904 private function setTypes(?Type $type, ?Type $phpDocType): void 905 { 906 $this->type = $type; 907 $this->phpDocType = $phpDocType; 908 } 909} 910 911interface ConstOrClassConstName { 912 public function __toString(): string; 913 public function equals(ConstOrClassConstName $const): bool; 914 public function isClassConst(): bool; 915 public function isUnknown(): bool; 916} 917 918abstract class AbstractConstName implements ConstOrClassConstName 919{ 920 public function equals(ConstOrClassConstName $const): bool 921 { 922 return $this->__toString() === $const->__toString(); 923 } 924 925 public function isUnknown(): bool 926 { 927 return strtolower($this->__toString()) === "unknown"; 928 } 929} 930 931class ConstName extends AbstractConstName { 932 public string $const; 933 934 public function __construct(?Name $namespace, string $const) 935 { 936 if ($namespace && ($namespace = $namespace->slice(0, -1))) { 937 $const = $namespace->toString() . '\\' . $const; 938 } 939 $this->const = $const; 940 } 941 942 public function isClassConst(): bool 943 { 944 return false; 945 } 946 947 public function isUnknown(): bool 948 { 949 $name = $this->__toString(); 950 if (($pos = strrpos($name, '\\')) !== false) { 951 $name = substr($name, $pos + 1); 952 } 953 return strtolower($name) === "unknown"; 954 } 955 956 public function __toString(): string 957 { 958 return $this->const; 959 } 960} 961 962class ClassConstName extends AbstractConstName { 963 public Name $class; 964 public string $const; 965 966 public function __construct(Name $class, string $const) 967 { 968 $this->class = $class; 969 $this->const = $const; 970 } 971 972 public function isClassConst(): bool 973 { 974 return true; 975 } 976 977 public function __toString(): string 978 { 979 return $this->class->toString() . "::" . $this->const; 980 } 981} 982 983class PropertyName { 984 public Name $class; 985 public string $property; 986 987 public function __construct(Name $class, string $property) 988 { 989 $this->class = $class; 990 $this->property = $property; 991 } 992 993 public function __toString(): string 994 { 995 return $this->class->toString() . "::$" . $this->property; 996 } 997} 998 999interface FunctionOrMethodName { 1000 public function getDeclaration(): string; 1001 public function getArgInfoName(): string; 1002 public function getMethodSynopsisFilename(): string; 1003 public function getNameForAttributes(): string; 1004 public function __toString(): string; 1005 public function isMethod(): bool; 1006 public function isConstructor(): bool; 1007 public function isDestructor(): bool; 1008} 1009 1010class FunctionName implements FunctionOrMethodName { 1011 private Name $name; 1012 1013 public function __construct(Name $name) { 1014 $this->name = $name; 1015 } 1016 1017 public function getNamespace(): ?string { 1018 if ($this->name->isQualified()) { 1019 return $this->name->slice(0, -1)->toString(); 1020 } 1021 return null; 1022 } 1023 1024 public function getNonNamespacedName(): string { 1025 if ($this->name->isQualified()) { 1026 throw new Exception("Namespaced name not supported here"); 1027 } 1028 return $this->name->toString(); 1029 } 1030 1031 public function getDeclarationName(): string { 1032 return implode('_', $this->name->parts); 1033 } 1034 1035 public function getFunctionName(): string { 1036 return $this->name->getLast(); 1037 } 1038 1039 public function getDeclaration(): string { 1040 return "ZEND_FUNCTION({$this->getDeclarationName()});\n"; 1041 } 1042 1043 public function getArgInfoName(): string { 1044 $underscoreName = implode('_', $this->name->parts); 1045 return "arginfo_$underscoreName"; 1046 } 1047 1048 public function getMethodSynopsisFilename(): string { 1049 return implode('_', $this->name->parts); 1050 } 1051 1052 public function getNameForAttributes(): string { 1053 return strtolower($this->name->toString()); 1054 } 1055 1056 public function __toString(): string { 1057 return $this->name->toString(); 1058 } 1059 1060 public function isMethod(): bool { 1061 return false; 1062 } 1063 1064 public function isConstructor(): bool { 1065 return false; 1066 } 1067 1068 public function isDestructor(): bool { 1069 return false; 1070 } 1071} 1072 1073class MethodName implements FunctionOrMethodName { 1074 public Name $className; 1075 public string $methodName; 1076 1077 public function __construct(Name $className, string $methodName) { 1078 $this->className = $className; 1079 $this->methodName = $methodName; 1080 } 1081 1082 public function getDeclarationClassName(): string { 1083 return implode('_', $this->className->parts); 1084 } 1085 1086 public function getDeclaration(): string { 1087 return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; 1088 } 1089 1090 public function getArgInfoName(): string { 1091 return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; 1092 } 1093 1094 public function getMethodSynopsisFilename(): string { 1095 return $this->getDeclarationClassName() . "_{$this->methodName}"; 1096 } 1097 1098 public function getNameForAttributes(): string { 1099 return strtolower($this->methodName); 1100 } 1101 1102 public function __toString(): string { 1103 return "$this->className::$this->methodName"; 1104 } 1105 1106 public function isMethod(): bool { 1107 return true; 1108 } 1109 1110 public function isConstructor(): bool { 1111 return $this->methodName === "__construct"; 1112 } 1113 1114 public function isDestructor(): bool { 1115 return $this->methodName === "__destruct"; 1116 } 1117} 1118 1119class ReturnInfo { 1120 const REFCOUNT_0 = "0"; 1121 const REFCOUNT_1 = "1"; 1122 const REFCOUNT_N = "N"; 1123 1124 const REFCOUNTS = [ 1125 self::REFCOUNT_0, 1126 self::REFCOUNT_1, 1127 self::REFCOUNT_N, 1128 ]; 1129 1130 public bool $byRef; 1131 public ?Type $type; 1132 public ?Type $phpDocType; 1133 public bool $tentativeReturnType; 1134 public string $refcount; 1135 1136 public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType, bool $tentativeReturnType, ?string $refcount) { 1137 $this->byRef = $byRef; 1138 $this->setTypes($type, $phpDocType, $tentativeReturnType); 1139 $this->setRefcount($refcount); 1140 } 1141 1142 public function equalsApartFromPhpDocAndRefcount(ReturnInfo $other): bool { 1143 return $this->byRef === $other->byRef 1144 && Type::equals($this->type, $other->type) 1145 && $this->tentativeReturnType === $other->tentativeReturnType; 1146 } 1147 1148 public function getMethodSynopsisType(): ?Type { 1149 return $this->type ?? $this->phpDocType; 1150 } 1151 1152 private function setTypes(?Type $type, ?Type $phpDocType, bool $tentativeReturnType): void 1153 { 1154 $this->type = $type; 1155 $this->phpDocType = $phpDocType; 1156 $this->tentativeReturnType = $tentativeReturnType; 1157 } 1158 1159 private function setRefcount(?string $refcount): void 1160 { 1161 $type = $this->phpDocType ?? $this->type; 1162 $isScalarType = $type !== null && $type->isScalar(); 1163 1164 if ($refcount === null) { 1165 $this->refcount = $isScalarType ? self::REFCOUNT_0 : self::REFCOUNT_N; 1166 return; 1167 } 1168 1169 if (!in_array($refcount, ReturnInfo::REFCOUNTS, true)) { 1170 throw new Exception("@refcount must have one of the following values: \"0\", \"1\", \"N\", $refcount given"); 1171 } 1172 1173 if ($isScalarType && $refcount !== self::REFCOUNT_0) { 1174 throw new Exception('A scalar return type of "' . $type->__toString() . '" must have a refcount of "' . self::REFCOUNT_0 . '"'); 1175 } 1176 1177 if (!$isScalarType && $refcount === self::REFCOUNT_0) { 1178 throw new Exception('A non-scalar return type of "' . $type->__toString() . '" cannot have a refcount of "' . self::REFCOUNT_0 . '"'); 1179 } 1180 1181 $this->refcount = $refcount; 1182 } 1183} 1184 1185class FuncInfo { 1186 public FunctionOrMethodName $name; 1187 public int $classFlags; 1188 public int $flags; 1189 public ?string $aliasType; 1190 public ?FunctionOrMethodName $alias; 1191 public bool $isDeprecated; 1192 public bool $supportsCompileTimeEval; 1193 public bool $verify; 1194 /** @var ArgInfo[] */ 1195 public array $args; 1196 public ReturnInfo $return; 1197 public int $numRequiredArgs; 1198 public ?string $cond; 1199 public bool $isUndocumentable; 1200 1201 /** 1202 * @param ArgInfo[] $args 1203 */ 1204 public function __construct( 1205 FunctionOrMethodName $name, 1206 int $classFlags, 1207 int $flags, 1208 ?string $aliasType, 1209 ?FunctionOrMethodName $alias, 1210 bool $isDeprecated, 1211 bool $supportsCompileTimeEval, 1212 bool $verify, 1213 array $args, 1214 ReturnInfo $return, 1215 int $numRequiredArgs, 1216 ?string $cond, 1217 bool $isUndocumentable 1218 ) { 1219 $this->name = $name; 1220 $this->classFlags = $classFlags; 1221 $this->flags = $flags; 1222 $this->aliasType = $aliasType; 1223 $this->alias = $alias; 1224 $this->isDeprecated = $isDeprecated; 1225 $this->supportsCompileTimeEval = $supportsCompileTimeEval; 1226 $this->verify = $verify; 1227 $this->args = $args; 1228 $this->return = $return; 1229 $this->numRequiredArgs = $numRequiredArgs; 1230 $this->cond = $cond; 1231 $this->isUndocumentable = $isUndocumentable; 1232 } 1233 1234 public function isMethod(): bool 1235 { 1236 return $this->name->isMethod(); 1237 } 1238 1239 public function isFinalMethod(): bool 1240 { 1241 return ($this->flags & Class_::MODIFIER_FINAL) || ($this->classFlags & Class_::MODIFIER_FINAL); 1242 } 1243 1244 public function isInstanceMethod(): bool 1245 { 1246 return !($this->flags & Class_::MODIFIER_STATIC) && $this->isMethod() && !$this->name->isConstructor(); 1247 } 1248 1249 /** @return string[] */ 1250 public function getModifierNames(): array 1251 { 1252 if (!$this->isMethod()) { 1253 return []; 1254 } 1255 1256 $result = []; 1257 1258 if ($this->flags & Class_::MODIFIER_FINAL) { 1259 $result[] = "final"; 1260 } elseif ($this->flags & Class_::MODIFIER_ABSTRACT && $this->classFlags & ~Class_::MODIFIER_ABSTRACT) { 1261 $result[] = "abstract"; 1262 } 1263 1264 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1265 $result[] = "protected"; 1266 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1267 $result[] = "private"; 1268 } else { 1269 $result[] = "public"; 1270 } 1271 1272 if ($this->flags & Class_::MODIFIER_STATIC) { 1273 $result[] = "static"; 1274 } 1275 1276 return $result; 1277 } 1278 1279 public function hasParamWithUnknownDefaultValue(): bool 1280 { 1281 foreach ($this->args as $arg) { 1282 if ($arg->defaultValue && !$arg->hasProperDefaultValue()) { 1283 return true; 1284 } 1285 } 1286 1287 return false; 1288 } 1289 1290 public function equalsApartFromNameAndRefcount(FuncInfo $other): bool { 1291 if (count($this->args) !== count($other->args)) { 1292 return false; 1293 } 1294 1295 for ($i = 0; $i < count($this->args); $i++) { 1296 if (!$this->args[$i]->equals($other->args[$i])) { 1297 return false; 1298 } 1299 } 1300 1301 return $this->return->equalsApartFromPhpDocAndRefcount($other->return) 1302 && $this->numRequiredArgs === $other->numRequiredArgs 1303 && $this->cond === $other->cond; 1304 } 1305 1306 public function getArgInfoName(): string { 1307 return $this->name->getArgInfoName(); 1308 } 1309 1310 public function getDeclarationKey(): string 1311 { 1312 $name = $this->alias ?? $this->name; 1313 1314 return "$name|$this->cond"; 1315 } 1316 1317 public function getDeclaration(): ?string 1318 { 1319 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1320 return null; 1321 } 1322 1323 $name = $this->alias ?? $this->name; 1324 1325 return $name->getDeclaration(); 1326 } 1327 1328 public function getFunctionEntry(): string { 1329 if ($this->name instanceof MethodName) { 1330 if ($this->alias) { 1331 if ($this->alias instanceof MethodName) { 1332 return sprintf( 1333 "\tZEND_MALIAS(%s, %s, %s, %s, %s)\n", 1334 $this->alias->getDeclarationClassName(), $this->name->methodName, 1335 $this->alias->methodName, $this->getArgInfoName(), $this->getFlagsAsArginfoString() 1336 ); 1337 } else if ($this->alias instanceof FunctionName) { 1338 return sprintf( 1339 "\tZEND_ME_MAPPING(%s, %s, %s, %s)\n", 1340 $this->name->methodName, $this->alias->getNonNamespacedName(), 1341 $this->getArgInfoName(), $this->getFlagsAsArginfoString() 1342 ); 1343 } else { 1344 throw new Error("Cannot happen"); 1345 } 1346 } else { 1347 $declarationClassName = $this->name->getDeclarationClassName(); 1348 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1349 return sprintf( 1350 "\tZEND_ABSTRACT_ME_WITH_FLAGS(%s, %s, %s, %s)\n", 1351 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 1352 $this->getFlagsAsArginfoString() 1353 ); 1354 } 1355 1356 return sprintf( 1357 "\tZEND_ME(%s, %s, %s, %s)\n", 1358 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 1359 $this->getFlagsAsArginfoString() 1360 ); 1361 } 1362 } else if ($this->name instanceof FunctionName) { 1363 $namespace = $this->name->getNamespace(); 1364 $functionName = $this->name->getFunctionName(); 1365 $declarationName = $this->alias ? $this->alias->getNonNamespacedName() : $this->name->getDeclarationName(); 1366 1367 if ($namespace) { 1368 // Namespaced functions are always declared as aliases to avoid name conflicts when two functions with 1369 // the same name exist in separate namespaces 1370 $macro = $this->isDeprecated ? 'ZEND_NS_DEP_FALIAS' : 'ZEND_NS_FALIAS'; 1371 1372 // Render A\B as "A\\B" in C strings for namespaces 1373 return sprintf( 1374 "\t%s(\"%s\", %s, %s, %s)\n", 1375 $macro, addslashes($namespace), $this->name->getFunctionName(), $declarationName, $this->getArgInfoName() 1376 ); 1377 } 1378 1379 if ($this->alias) { 1380 $macro = $this->isDeprecated ? 'ZEND_DEP_FALIAS' : 'ZEND_FALIAS'; 1381 1382 return sprintf( 1383 "\t%s(%s, %s, %s)\n", 1384 $macro, $functionName, $declarationName, $this->getArgInfoName() 1385 ); 1386 } 1387 1388 switch (true) { 1389 case $this->isDeprecated: 1390 $macro = 'ZEND_DEP_FE'; 1391 break; 1392 case $this->supportsCompileTimeEval: 1393 $macro = 'ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE'; 1394 break; 1395 default: 1396 $macro = 'ZEND_FE'; 1397 } 1398 1399 return sprintf("\t%s(%s, %s)\n", $macro, $functionName, $this->getArgInfoName()); 1400 } else { 1401 throw new Error("Cannot happen"); 1402 } 1403 } 1404 1405 public function getOptimizerInfo(): ?string { 1406 if ($this->isMethod()) { 1407 return null; 1408 } 1409 1410 if ($this->alias !== null) { 1411 return null; 1412 } 1413 1414 if ($this->return->refcount !== ReturnInfo::REFCOUNT_1 && $this->return->phpDocType === null) { 1415 return null; 1416 } 1417 1418 $type = $this->return->phpDocType ?? $this->return->type; 1419 if ($type === null) { 1420 return null; 1421 } 1422 1423 return "\tF" . $this->return->refcount . '("' . $this->name->__toString() . '", ' . $type->toOptimizerTypeMask() . "),\n"; 1424 } 1425 1426 public function discardInfoForOldPhpVersions(): void { 1427 $this->return->type = null; 1428 foreach ($this->args as $arg) { 1429 $arg->type = null; 1430 $arg->defaultValue = null; 1431 $arg->attributes = []; 1432 } 1433 } 1434 1435 private function getFlagsAsArginfoString(): string 1436 { 1437 $flags = "ZEND_ACC_PUBLIC"; 1438 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1439 $flags = "ZEND_ACC_PROTECTED"; 1440 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1441 $flags = "ZEND_ACC_PRIVATE"; 1442 } 1443 1444 if ($this->flags & Class_::MODIFIER_STATIC) { 1445 $flags .= "|ZEND_ACC_STATIC"; 1446 } 1447 1448 if ($this->flags & Class_::MODIFIER_FINAL) { 1449 $flags .= "|ZEND_ACC_FINAL"; 1450 } 1451 1452 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1453 $flags .= "|ZEND_ACC_ABSTRACT"; 1454 } 1455 1456 if ($this->isDeprecated) { 1457 $flags .= "|ZEND_ACC_DEPRECATED"; 1458 } 1459 1460 return $flags; 1461 } 1462 1463 /** 1464 * @param array<string, FuncInfo> $funcMap 1465 * @param array<string, FuncInfo> $aliasMap 1466 * @throws Exception 1467 */ 1468 public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string { 1469 1470 $doc = new DOMDocument(); 1471 $doc->formatOutput = true; 1472 $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 1473 if (!$methodSynopsis) { 1474 return null; 1475 } 1476 1477 $doc->appendChild($methodSynopsis); 1478 1479 return $doc->saveXML(); 1480 } 1481 1482 /** 1483 * @param array<string, FuncInfo> $funcMap 1484 * @param array<string, FuncInfo> $aliasMap 1485 * @throws Exception 1486 */ 1487 public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement { 1488 if ($this->hasParamWithUnknownDefaultValue()) { 1489 return null; 1490 } 1491 1492 if ($this->name->isConstructor()) { 1493 $synopsisType = "constructorsynopsis"; 1494 } elseif ($this->name->isDestructor()) { 1495 $synopsisType = "destructorsynopsis"; 1496 } else { 1497 $synopsisType = "methodsynopsis"; 1498 } 1499 1500 $methodSynopsis = $doc->createElement($synopsisType); 1501 1502 if ($this->isMethod()) { 1503 assert($this->name instanceof MethodName); 1504 $role = $doc->createAttribute("role"); 1505 $role->value = addslashes($this->name->className->__toString()); 1506 $methodSynopsis->appendChild($role); 1507 } 1508 1509 $methodSynopsis->appendChild(new DOMText("\n ")); 1510 1511 foreach ($this->getModifierNames() as $modifierString) { 1512 $modifierElement = $doc->createElement('modifier', $modifierString); 1513 $methodSynopsis->appendChild($modifierElement); 1514 $methodSynopsis->appendChild(new DOMText(" ")); 1515 } 1516 1517 $returnType = $this->return->getMethodSynopsisType(); 1518 if ($returnType) { 1519 $methodSynopsis->appendChild($returnType->getTypeForDoc($doc)); 1520 } 1521 1522 $methodname = $doc->createElement('methodname', $this->name->__toString()); 1523 $methodSynopsis->appendChild($methodname); 1524 1525 if (empty($this->args)) { 1526 $methodSynopsis->appendChild(new DOMText("\n ")); 1527 $void = $doc->createElement('void'); 1528 $methodSynopsis->appendChild($void); 1529 } else { 1530 foreach ($this->args as $arg) { 1531 $methodSynopsis->appendChild(new DOMText("\n ")); 1532 $methodparam = $doc->createElement('methodparam'); 1533 if ($arg->defaultValue !== null) { 1534 $methodparam->setAttribute("choice", "opt"); 1535 } 1536 if ($arg->isVariadic) { 1537 $methodparam->setAttribute("rep", "repeat"); 1538 } 1539 1540 $methodSynopsis->appendChild($methodparam); 1541 $methodparam->appendChild($arg->getMethodSynopsisType()->getTypeForDoc($doc)); 1542 1543 $parameter = $doc->createElement('parameter', $arg->name); 1544 if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) { 1545 $parameter->setAttribute("role", "reference"); 1546 } 1547 1548 $methodparam->appendChild($parameter); 1549 $defaultValue = $arg->getDefaultValueAsMethodSynopsisString(); 1550 if ($defaultValue !== null) { 1551 $initializer = $doc->createElement('initializer'); 1552 if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) { 1553 $constant = $doc->createElement('constant', $defaultValue); 1554 $initializer->appendChild($constant); 1555 } else { 1556 $initializer->nodeValue = $defaultValue; 1557 } 1558 $methodparam->appendChild($initializer); 1559 } 1560 } 1561 } 1562 $methodSynopsis->appendChild(new DOMText("\n ")); 1563 1564 return $methodSynopsis; 1565 } 1566 1567 public function __clone() 1568 { 1569 foreach ($this->args as $key => $argInfo) { 1570 $this->args[$key] = clone $argInfo; 1571 } 1572 $this->return = clone $this->return; 1573 } 1574} 1575 1576class EvaluatedValue 1577{ 1578 /** @var mixed */ 1579 public $value; 1580 public SimpleType $type; 1581 public ?string $cConstValue; 1582 public bool $isUnknownConstValue; 1583 public ?ConstInfo $originatingConst; 1584 1585 /** 1586 * @param iterable<ConstInfo> $allConstInfos 1587 */ 1588 public static function createFromExpression(Expr $expr, ?SimpleType $constType, ?string $cConstName, iterable $allConstInfos): EvaluatedValue 1589 { 1590 $originatingConst = null; 1591 $isUnknownConstValue = null; 1592 1593 $evaluator = new ConstExprEvaluator( 1594 function (Expr $expr) use ($allConstInfos, &$constType, &$originatingConst, &$isUnknownConstValue) { 1595 if (!$expr instanceof Expr\ConstFetch && !$expr instanceof Expr\ClassConstFetch) { 1596 throw new Exception($this->getVariableTypeName() . " " . $this->getVariableLikeName() . " has an unsupported value"); 1597 } 1598 1599 if ($expr instanceof Expr\ClassConstFetch) { 1600 $originatingConstName = new ClassConstName($expr->class, $expr->name->toString()); 1601 } else { 1602 $originatingConstName = new ConstName($expr->name->getAttribute('namespacedName'), $expr->name->toString()); 1603 } 1604 1605 if ($originatingConstName->isUnknown()) { 1606 $originatingConst = null; 1607 $isUnknownConstValue = true; 1608 1609 return null; 1610 } 1611 1612 foreach ($allConstInfos as $const) { 1613 if (!$originatingConstName->equals($const->name)) { 1614 continue; 1615 } 1616 1617 if ($constType === null && $const->phpDocType) { 1618 $constType = $const->phpDocType->tryToSimpleType(); 1619 } 1620 1621 $originatingConst = $const; 1622 $isUnknownConstValue = false; 1623 1624 return null; 1625 } 1626 1627 throw new Exception("Constant " . $constName . " cannot be found"); 1628 } 1629 ); 1630 1631 $result = $evaluator->evaluateDirectly($expr); 1632 1633 return new EvaluatedValue( 1634 $result, 1635 $constType ?: SimpleType::fromValue($result), 1636 $cConstName, 1637 $originatingConst, 1638 (bool) $isUnknownConstValue 1639 ); 1640 } 1641 1642 public static function null(): EvaluatedValue 1643 { 1644 return new self(null, SimpleType::null(), null, null, false); 1645 } 1646 1647 /** 1648 * @param mixed $value 1649 */ 1650 private function __construct($value, SimpleType $type, ?string $cConstName, ?ConstInfo $originatingConst, bool $isUnknownConstValue) 1651 { 1652 $this->value = $value; 1653 $this->type = $type; 1654 $this->cConstValue = $cConstName; 1655 $this->originatingConst = $originatingConst; 1656 $this->isUnknownConstValue = $isUnknownConstValue; 1657 } 1658 1659 /** 1660 * @param iterable<ConstInfo> $allConstInfos 1661 */ 1662 public function initializeZval(string $zvalName, iterable $allConstInfos): string 1663 { 1664 $cConstValue = $this->getCConstValue($allConstInfos); 1665 1666 $code = "\tzval $zvalName;\n"; 1667 1668 if ($this->type->isNull()) { 1669 $code .= "\tZVAL_NULL(&$zvalName);\n"; 1670 } elseif ($this->type->isBool()) { 1671 $code .= "\t" . ($this->value ? 'ZVAL_TRUE' : 'ZVAL_FALSE') . "(&$zvalName);\n"; 1672 } elseif ($this->type->isInt()) { 1673 $code .= "\tZVAL_LONG(&$zvalName, " . ($cConstValue ?: $this->value) . ");\n"; 1674 } elseif ($this->type->isFloat()) { 1675 $code .= "\tZVAL_DOUBLE(&$zvalName, " . ($cConstValue ?: $this->value) . ");\n"; 1676 } elseif ($this->type->isString()) { 1677 if (!$cConstValue && $this->value === "") { 1678 $code .= "\tZVAL_EMPTY_STRING(&$zvalName);\n"; 1679 } else { 1680 $constValue = $cConstValue ?: '"' . addslashes($this->value) . '"'; 1681 $code .= "\tzend_string *{$zvalName}_str = zend_string_init($constValue, strlen($constValue), 1);\n"; 1682 $code .= "\tZVAL_STR(&$zvalName, {$zvalName}_str);\n"; 1683 } 1684 } elseif ($this->type->isArray()) { 1685 if (!$cConstValue && empty($this->value)) { 1686 $code .= "\tZVAL_EMPTY_ARRAY(&$zvalName);\n"; 1687 } else { 1688 throw new Exception("Unimplemented default value"); 1689 } 1690 } else { 1691 throw new Exception("Invalid default value"); 1692 } 1693 1694 return $code; 1695 } 1696 1697 /** 1698 * @param iterable<ConstInfo> $allConstInfos 1699 */ 1700 public function getCConstValue(iterable $allConstInfos): ?string 1701 { 1702 if ($this->cConstValue) { 1703 return $this->cConstValue; 1704 } 1705 1706 if ($this->originatingConst) { 1707 return $this->originatingConst->getValue($allConstInfos)->getCConstValue($allConstInfos); 1708 } 1709 1710 return null; 1711 } 1712} 1713 1714abstract class VariableLike 1715{ 1716 public ?Type $phpDocType; 1717 public int $flags; 1718 public ?string $link; 1719 public ?int $phpVersionIdMinimumCompatibility; 1720 1721 public function __construct( 1722 int $flags, 1723 ?Type $phpDocType, 1724 ?string $link, 1725 ?int $phpVersionIdMinimumCompatibility 1726 ) { 1727 $this->flags = $flags; 1728 $this->phpDocType = $phpDocType; 1729 $this->link = $link; 1730 $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; 1731 } 1732 1733 abstract protected function getVariableTypeCode(): string; 1734 1735 abstract protected function getVariableTypeName(): string; 1736 1737 abstract protected function getVariableLikeName(): string; 1738 1739 abstract protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void; 1740 1741 abstract protected function getFieldSynopsisDefaultLinkend(): string; 1742 1743 abstract protected function getFieldSynopsisName(): string; 1744 1745 /** 1746 * @param iterable<ConstInfo> $allConstInfos 1747 */ 1748 abstract protected function getFieldSynopsisValueString(iterable $allConstInfos): ?string; 1749 1750 abstract public function discardInfoForOldPhpVersions(): void; 1751 1752 /** 1753 * @return array<int, string[]> 1754 */ 1755 protected function getFlagsByPhpVersion(): array 1756 { 1757 $flags = "ZEND_ACC_PUBLIC"; 1758 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1759 $flags = "ZEND_ACC_PROTECTED"; 1760 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1761 $flags = "ZEND_ACC_PRIVATE"; 1762 } 1763 1764 return [ 1765 PHP_70_VERSION_ID => [$flags], 1766 PHP_80_VERSION_ID => [$flags], 1767 PHP_81_VERSION_ID => [$flags], 1768 PHP_82_VERSION_ID => [$flags], 1769 ]; 1770 } 1771 1772 /** 1773 * @param iterable<ConstInfo> $allConstInfos 1774 */ 1775 public function getFieldSynopsisElement(DOMDocument $doc, iterable $allConstInfos): DOMElement 1776 { 1777 $fieldsynopsisElement = $doc->createElement("fieldsynopsis"); 1778 1779 $this->addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 1780 1781 $this->addTypeToFieldSynopsis($doc, $fieldsynopsisElement); 1782 1783 $varnameElement = $doc->createElement("varname", $this->getFieldSynopsisName()); 1784 if ($this->link) { 1785 $varnameElement->setAttribute("linkend", $this->link); 1786 } else { 1787 $varnameElement->setAttribute("linkend", $this->getFieldSynopsisDefaultLinkend()); 1788 } 1789 1790 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1791 $fieldsynopsisElement->appendChild($varnameElement); 1792 1793 $valueString = $this->getFieldSynopsisValueString($allConstInfos); 1794 if ($valueString) { 1795 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1796 $initializerElement = $doc->createElement("initializer", $valueString); 1797 $fieldsynopsisElement->appendChild($initializerElement); 1798 } 1799 1800 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1801 1802 return $fieldsynopsisElement; 1803 } 1804 1805 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 1806 { 1807 if ($this->flags & Class_::MODIFIER_PUBLIC) { 1808 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1809 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "public")); 1810 } elseif ($this->flags & Class_::MODIFIER_PROTECTED) { 1811 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1812 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected")); 1813 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1814 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1815 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private")); 1816 } 1817 } 1818 1819 /** 1820 * @param array<int, string[]> $flags 1821 * @return array<int, string[]> 1822 */ 1823 protected function addFlagForVersionsAbove(array $flags, string $flag, int $minimumVersionId): array 1824 { 1825 $write = false; 1826 1827 foreach ($flags as $version => $versionFlags) { 1828 if ($version === $minimumVersionId || $write === true) { 1829 $flags[$version][] = $flag; 1830 $write = true; 1831 } 1832 } 1833 1834 return $flags; 1835 } 1836} 1837 1838class ConstInfo extends VariableLike 1839{ 1840 public ConstOrClassConstName $name; 1841 public Expr $value; 1842 public bool $isDeprecated; 1843 public ?string $valueString; 1844 public ?string $cond; 1845 public ?string $cValue; 1846 1847 public function __construct( 1848 ConstOrClassConstName $name, 1849 int $flags, 1850 Expr $value, 1851 ?string $valueString, 1852 ?Type $phpDocType, 1853 bool $isDeprecated, 1854 ?string $cond, 1855 ?string $cValue, 1856 ?string $link, 1857 ?int $phpVersionIdMinimumCompatibility 1858 ) { 1859 $this->name = $name; 1860 $this->value = $value; 1861 $this->valueString = $valueString; 1862 $this->isDeprecated = $isDeprecated; 1863 $this->cond = $cond; 1864 $this->cValue = $cValue; 1865 parent::__construct($flags, $phpDocType, $link, $phpVersionIdMinimumCompatibility); 1866 } 1867 1868 /** 1869 * @param iterable<ConstInfo> $allConstInfos 1870 */ 1871 public function getValue(iterable $allConstInfos): EvaluatedValue 1872 { 1873 return EvaluatedValue::createFromExpression( 1874 $this->value, 1875 $this->phpDocType->tryToSimpleType(), 1876 $this->cValue, 1877 $allConstInfos 1878 ); 1879 } 1880 1881 protected function getVariableTypeName(): string 1882 { 1883 return "constant"; 1884 } 1885 1886 protected function getVariableLikeName(): string 1887 { 1888 return $this->name->const; 1889 } 1890 1891 protected function getVariableTypeCode(): string 1892 { 1893 return "const"; 1894 } 1895 1896 protected function getFieldSynopsisDefaultLinkend(): string 1897 { 1898 $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); 1899 1900 return "$className.constants." . strtolower(str_replace("_", "-", $this->name->const)); 1901 } 1902 1903 protected function getFieldSynopsisName(): string 1904 { 1905 return $this->name->__toString(); 1906 } 1907 1908 protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 1909 { 1910 if ($this->phpDocType) { 1911 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1912 $fieldsynopsisElement->appendChild($this->phpDocType->getTypeForDoc($doc)); 1913 } 1914 } 1915 1916 /** 1917 * @param iterable<ConstInfo> $allConstInfos 1918 */ 1919 protected function getFieldSynopsisValueString(iterable $allConstInfos): ?string 1920 { 1921 $value = EvaluatedValue::createFromExpression($this->value, null, $this->cValue, $allConstInfos); 1922 if ($value->isUnknownConstValue) { 1923 return null; 1924 } 1925 1926 if ($value->originatingConst) { 1927 return $value->originatingConst->getFieldSynopsisValueString($allConstInfos); 1928 } 1929 1930 return $this->valueString; 1931 } 1932 1933 public function discardInfoForOldPhpVersions(): void { 1934 $this->flags &= ~Class_::MODIFIER_FINAL; 1935 $this->isDeprecated = false; 1936 } 1937 1938 /** 1939 * @param iterable<ConstInfo> $allConstInfos 1940 */ 1941 public function getDeclaration(iterable $allConstInfos): string 1942 { 1943 $type = $this->phpDocType->tryToSimpleType(); 1944 if ($type === null || !$type->isBuiltin) { 1945 throw new Exception("Constant " . $this->name->__toString() . " must have a simple built-in type"); 1946 } 1947 1948 $value = EvaluatedValue::createFromExpression($this->value, $type, $this->cValue, $allConstInfos); 1949 if ($value->isUnknownConstValue && !$value->cConstValue) { 1950 throw new Exception("Constant " . $this->name->__toString() . " must have a @cvalue annotation"); 1951 } 1952 1953 $code = ""; 1954 1955 if ($this->cond) { 1956 $code .= "#if {$this->cond}\n"; 1957 } 1958 1959 if ($this->name->isClassConst()) { 1960 $code .= $this->getClassConstDeclaration($value, $allConstInfos); 1961 } else { 1962 $code .= $this->getGlobalConstDeclaration($value, $allConstInfos); 1963 } 1964 $code .= $this->getValueAssertion($value); 1965 1966 if ($this->cond) { 1967 $code .= "#endif\n"; 1968 } 1969 1970 return $code; 1971 } 1972 1973 /** 1974 * @param iterable<ConstInfo> $allConstInfos 1975 */ 1976 private function getGlobalConstDeclaration(EvaluatedValue $value, iterable $allConstInfos): string 1977 { 1978 $constName = str_replace('\\', '\\\\', $this->name->__toString()); 1979 $constValue = $value->value; 1980 $cConstValue = $value->getCConstValue($allConstInfos); 1981 1982 $flags = "CONST_PERSISTENT"; 1983 if ($this->phpVersionIdMinimumCompatibility !== null && $this->phpVersionIdMinimumCompatibility < 80000) { 1984 $flags .= " | CONST_CS"; 1985 } 1986 1987 if ($this->isDeprecated) { 1988 $flags .= " | CONST_DEPRECATED"; 1989 } 1990 if ($value->type->isNull()) { 1991 return "\tREGISTER_NULL_CONSTANT(\"$constName\", $flags);\n"; 1992 } 1993 1994 if ($value->type->isBool()) { 1995 return "\tREGISTER_BOOL_CONSTANT(\"$constName\", " . ($cConstValue ?: ($constValue ? "true" : "false")) . ", $flags);\n"; 1996 } 1997 1998 if ($value->type->isInt()) { 1999 return "\tREGISTER_LONG_CONSTANT(\"$constName\", " . ($cConstValue ?: (int) $constValue) . ", $flags);\n"; 2000 } 2001 2002 if ($value->type->isFloat()) { 2003 return "\tREGISTER_DOUBLE_CONSTANT(\"$constName\", " . ($cConstValue ?: (float) $constValue) . ", $flags);\n"; 2004 } 2005 2006 if ($value->type->isString()) { 2007 return "\tREGISTER_STRING_CONSTANT(\"$constName\", " . ($cConstValue ?: '"' . addslashes($constValue) . '"') . ", $flags);\n"; 2008 } 2009 2010 throw new Exception("Unimplemented constant type");} 2011 2012 /** 2013 * @param iterable<ConstInfo> $allConstInfos 2014 */ 2015 private function getClassConstDeclaration(EvaluatedValue $value, iterable $allConstInfos): string 2016 { 2017 $constName = $this->getVariableLikeName(); 2018 2019 $zvalCode = $value->initializeZval("const_{$constName}_value", $allConstInfos); 2020 2021 $code = "\n" . $zvalCode; 2022 2023 $code .= "\tzend_string *const_{$constName}_name = zend_string_init_interned(\"$constName\", sizeof(\"$constName\") - 1, 1);\n"; 2024 $nameCode = "const_{$constName}_name"; 2025 2026 $template = "\tzend_declare_class_constant_ex(class_entry, $nameCode, &const_{$constName}_value, %s, NULL);\n"; 2027 $flagsCode = generateVersionDependentFlagCode( 2028 $template, 2029 $this->getFlagsByPhpVersion(), 2030 $this->phpVersionIdMinimumCompatibility 2031 ); 2032 $code .= implode("", $flagsCode); 2033 $code .= "\tzend_string_release(const_{$constName}_name);\n"; 2034 2035 return $code; 2036 } 2037 2038 private function getValueAssertion(EvaluatedValue $value): string 2039 { 2040 if ($value->isUnknownConstValue || $value->originatingConst || $value->cConstValue === null) { 2041 return ""; 2042 } 2043 2044 $cConstValue = $value->cConstValue; 2045 $constValue = $value->value; 2046 2047 if ($value->type->isNull()) { 2048 return "\tZEND_ASSERT($cConstValue == NULL);\n"; 2049 } 2050 2051 if ($value->type->isBool()) { 2052 $cValue = $constValue ? "true" : "false"; 2053 return "\tZEND_ASSERT($cConstValue == $cValue);\n"; 2054 } 2055 2056 if ($value->type->isInt()) { 2057 $cValue = (int) $constValue; 2058 return "\tZEND_ASSERT($cConstValue == $cValue);\n"; 2059 } 2060 2061 if ($value->type->isFloat()) { 2062 $cValue = (float) $constValue; 2063 return "\tZEND_ASSERT($cConstValue == $cValue);\n"; 2064 } 2065 2066 if ($value->type->isString()) { 2067 $cValue = '"' . addslashes($constValue) . '"'; 2068 return "\tZEND_ASSERT(strcmp($cConstValue, $cValue) == 0);\n"; 2069 } 2070 2071 throw new Exception("Unimplemented constant type"); 2072 } 2073 2074 /** 2075 * @return array<int, string[]> 2076 */ 2077 protected function getFlagsByPhpVersion(): array 2078 { 2079 $flags = parent::getFlagsByPhpVersion(); 2080 2081 if ($this->isDeprecated) { 2082 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID); 2083 } 2084 2085 if ($this->flags & Class_::MODIFIER_FINAL) { 2086 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_FINAL", PHP_81_VERSION_ID); 2087 } 2088 2089 return $flags; 2090 } 2091 2092 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2093 { 2094 parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 2095 2096 if ($this->flags & Class_::MODIFIER_FINAL) { 2097 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2098 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "final")); 2099 } 2100 2101 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2102 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "const")); 2103 } 2104} 2105 2106class PropertyInfo extends VariableLike 2107{ 2108 public PropertyName $name; 2109 public ?Type $type; 2110 public ?Expr $defaultValue; 2111 public ?string $defaultValueString; 2112 public bool $isDocReadonly; 2113 2114 public function __construct( 2115 PropertyName $name, 2116 int $flags, 2117 ?Type $type, 2118 ?Type $phpDocType, 2119 ?Expr $defaultValue, 2120 ?string $defaultValueString, 2121 bool $isDocReadonly, 2122 ?string $link, 2123 ?int $phpVersionIdMinimumCompatibility 2124 ) { 2125 $this->name = $name; 2126 $this->type = $type; 2127 $this->defaultValue = $defaultValue; 2128 $this->defaultValueString = $defaultValueString; 2129 $this->isDocReadonly = $isDocReadonly; 2130 parent::__construct($flags, $phpDocType, $link, $phpVersionIdMinimumCompatibility); 2131 } 2132 2133 protected function getVariableTypeCode(): string 2134 { 2135 return "property"; 2136 } 2137 2138 protected function getVariableTypeName(): string 2139 { 2140 return "property"; 2141 } 2142 2143 protected function getVariableLikeName(): string 2144 { 2145 return $this->name->property; 2146 } 2147 2148 protected function getFieldSynopsisDefaultLinkend(): string 2149 { 2150 $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); 2151 2152 return "$className.props." . strtolower(str_replace("_", "-", $this->name->property)); 2153 } 2154 2155 protected function getFieldSynopsisName(): string 2156 { 2157 return $this->name->property; 2158 } 2159 2160 /** 2161 * @param iterable<ConstInfo> $allConstInfos 2162 */ 2163 protected function getFieldSynopsisValueString(iterable $allConstInfos): ?string 2164 { 2165 return $this->defaultValueString; 2166 } 2167 2168 public function discardInfoForOldPhpVersions(): void { 2169 $this->type = null; 2170 $this->flags &= ~Class_::MODIFIER_READONLY; 2171 } 2172 2173 /** 2174 * @param iterable<ConstInfo> $allConstInfos 2175 */ 2176 public function getDeclaration(iterable $allConstInfos): string { 2177 $code = "\n"; 2178 2179 $propertyName = $this->name->property; 2180 2181 if ($this->defaultValue === null) { 2182 $defaultValue = EvaluatedValue::null(); 2183 } else { 2184 $defaultValue = EvaluatedValue::createFromExpression($this->defaultValue, null, null, $allConstInfos); 2185 if ($defaultValue->isUnknownConstValue || ($defaultValue->originatingConst && $defaultValue->getCConstValue($allConstInfos) === null)) { 2186 echo "Skipping code generation for property $this->name, because it has an unknown constant default value\n"; 2187 return ""; 2188 } 2189 } 2190 2191 $typeCode = ""; 2192 if ($this->type) { 2193 $arginfoType = $this->type->toArginfoType(); 2194 if ($arginfoType->hasClassType()) { 2195 if (count($arginfoType->classTypes) >= 2) { 2196 foreach ($arginfoType->classTypes as $classType) { 2197 $escapedClassName = $classType->toEscapedName(); 2198 $varEscapedClassName = $classType->toVarEscapedName(); 2199 $code .= "\tzend_string *property_{$propertyName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\") - 1, 1);\n"; 2200 } 2201 2202 $classTypeCount = count($arginfoType->classTypes); 2203 $code .= "\tzend_type_list *property_{$propertyName}_type_list = malloc(ZEND_TYPE_LIST_SIZE($classTypeCount));\n"; 2204 $code .= "\tproperty_{$propertyName}_type_list->num_types = $classTypeCount;\n"; 2205 2206 foreach ($arginfoType->classTypes as $k => $classType) { 2207 $escapedClassName = $classType->toEscapedName(); 2208 $code .= "\tproperty_{$propertyName}_type_list->types[$k] = (zend_type) ZEND_TYPE_INIT_CLASS(property_{$propertyName}_class_{$escapedClassName}, 0, 0);\n"; 2209 } 2210 2211 $typeMaskCode = $this->type->toArginfoType()->toTypeMask(); 2212 2213 if ($this->type->isIntersection) { 2214 $code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_INTERSECTION(property_{$propertyName}_type_list, $typeMaskCode);\n"; 2215 } else { 2216 $code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n"; 2217 } 2218 $typeCode = "property_{$propertyName}_type"; 2219 } else { 2220 $escapedClassName = $arginfoType->classTypes[0]->toEscapedName(); 2221 $varEscapedClassName = $arginfoType->classTypes[0]->toVarEscapedName(); 2222 $code .= "\tzend_string *property_{$propertyName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\")-1, 1);\n"; 2223 2224 $typeCode = "(zend_type) ZEND_TYPE_INIT_CLASS(property_{$propertyName}_class_{$varEscapedClassName}, 0, " . $arginfoType->toTypeMask() . ")"; 2225 } 2226 } else { 2227 $typeCode = "(zend_type) ZEND_TYPE_INIT_MASK(" . $arginfoType->toTypeMask() . ")"; 2228 } 2229 } 2230 2231 $zvalName = "property_{$this->name->property}_default_value"; 2232 if ($this->defaultValue === null && $this->type !== null) { 2233 $code .= "\tzval $zvalName;\n\tZVAL_UNDEF(&$zvalName);\n"; 2234 } else { 2235 $code .= $defaultValue->initializeZval($zvalName, $allConstInfos); 2236 } 2237 2238 $code .= "\tzend_string *property_{$propertyName}_name = zend_string_init(\"$propertyName\", sizeof(\"$propertyName\") - 1, 1);\n"; 2239 $nameCode = "property_{$propertyName}_name"; 2240 2241 if ($this->type !== null) { 2242 $template = "\tzend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, NULL, $typeCode);\n"; 2243 } else { 2244 $template = "\tzend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, NULL);\n"; 2245 } 2246 $flagsCode = generateVersionDependentFlagCode( 2247 $template, 2248 $this->getFlagsByPhpVersion(), 2249 $this->phpVersionIdMinimumCompatibility 2250 ); 2251 $code .= implode("", $flagsCode); 2252 2253 $code .= "\tzend_string_release(property_{$propertyName}_name);\n"; 2254 2255 return $code; 2256 } 2257 2258 /** 2259 * @return array<int, string[]> 2260 */ 2261 protected function getFlagsByPhpVersion(): array 2262 { 2263 $flags = parent::getFlagsByPhpVersion(); 2264 2265 if ($this->flags & Class_::MODIFIER_STATIC) { 2266 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_STATIC", PHP_70_VERSION_ID); 2267 } 2268 2269 if ($this->flags & Class_::MODIFIER_READONLY) { 2270 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_81_VERSION_ID); 2271 } 2272 2273 return $flags; 2274 } 2275 2276 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2277 { 2278 parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 2279 2280 if ($this->flags & Class_::MODIFIER_STATIC) { 2281 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2282 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "static")); 2283 } 2284 2285 if ($this->flags & Class_::MODIFIER_READONLY || $this->isDocReadonly) { 2286 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2287 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "readonly")); 2288 } 2289 } 2290 2291 protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2292 { 2293 $type = $this->phpDocType ?? $this->type; 2294 2295 if ($type) { 2296 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2297 $fieldsynopsisElement->appendChild($type->getTypeForDoc($doc)); 2298 } 2299 } 2300 2301 public function __clone() 2302 { 2303 if ($this->type) { 2304 $this->type = clone $this->type; 2305 } 2306 } 2307} 2308 2309class EnumCaseInfo { 2310 public string $name; 2311 public ?Expr $value; 2312 2313 public function __construct(string $name, ?Expr $value) { 2314 $this->name = $name; 2315 $this->value = $value; 2316 } 2317 2318 /** 2319 * @param iterable<ConstInfo> $allConstInfos 2320 */ 2321 public function getDeclaration(iterable $allConstInfos): string { 2322 $escapedName = addslashes($this->name); 2323 if ($this->value === null) { 2324 $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; 2325 } else { 2326 $value = EvaluatedValue::createFromExpression($this->value, null, null, $allConstInfos); 2327 2328 $zvalName = "enum_case_{$escapedName}_value"; 2329 $code = "\n" . $value->initializeZval($zvalName, $allConstInfos); 2330 $code .= "\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", &$zvalName);\n"; 2331 } 2332 2333 return $code; 2334 } 2335} 2336 2337class AttributeInfo { 2338 public string $class; 2339 /** @var \PhpParser\Node\Arg[] */ 2340 public array $args; 2341 2342 /** @param \PhpParser\Node\Arg[] $args */ 2343 public function __construct(string $class, array $args) { 2344 $this->class = $class; 2345 $this->args = $args; 2346 } 2347 2348 /** @param iterable<ConstInfo> $allConstInfos */ 2349 public function generateCode(string $invocation, string $nameSuffix, iterable $allConstInfos): string { 2350 /* see ZEND_KNOWN_STRINGS in Zend/strings.h */ 2351 static $knowns = [ 2352 "SensitiveParameter" => "ZEND_STR_SENSITIVEPARAMETER", 2353 ]; 2354 $code = "\n"; 2355 $escapedAttributeName = strtr($this->class, '\\', '_'); 2356 if (isset($knowns[$escapedAttributeName])) { 2357 $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, ZSTR_KNOWN({$knowns[$escapedAttributeName]}), " . count($this->args) . ");\n"; 2358 } else { 2359 $code .= "\tzend_string *attribute_name_{$escapedAttributeName}_$nameSuffix = zend_string_init_interned(\"" . addcslashes($this->class, "\\") . "\", sizeof(\"" . addcslashes($this->class, "\\") . "\") - 1, 1);\n"; 2360 $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, attribute_name_{$escapedAttributeName}_$nameSuffix, " . count($this->args) . ");\n"; 2361 $code .= "\tzend_string_release(attribute_name_{$escapedAttributeName}_$nameSuffix);\n"; 2362 } 2363 foreach ($this->args as $i => $arg) { 2364 $value = EvaluatedValue::createFromExpression($arg->value, null, null, $allConstInfos); 2365 $zvalName = "attribute_{$escapedAttributeName}_{$nameSuffix}_arg$i"; 2366 $code .= $value->initializeZval($zvalName, $allConstInfos); 2367 $code .= "\tZVAL_COPY_VALUE(&attribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].value, &$zvalName);\n"; 2368 if ($arg->name) { 2369 $code .= "\tattribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].name = zend_string_init(\"{$arg->name->name}\", sizeof(\"{$arg->name->name}\") - 1, 1);\n"; 2370 } 2371 } 2372 return $code; 2373 } 2374} 2375 2376class ClassInfo { 2377 public Name $name; 2378 public int $flags; 2379 public string $type; 2380 public ?string $alias; 2381 public ?SimpleType $enumBackingType; 2382 public bool $isDeprecated; 2383 public bool $isStrictProperties; 2384 /** @var AttributeInfo[] */ 2385 public array $attributes; 2386 public bool $isNotSerializable; 2387 /** @var Name[] */ 2388 public array $extends; 2389 /** @var Name[] */ 2390 public array $implements; 2391 /** @var ConstInfo[] */ 2392 public array $constInfos; 2393 /** @var PropertyInfo[] */ 2394 public array $propertyInfos; 2395 /** @var FuncInfo[] */ 2396 public array $funcInfos; 2397 /** @var EnumCaseInfo[] */ 2398 public array $enumCaseInfos; 2399 public ?string $cond; 2400 public ?int $phpVersionIdMinimumCompatibility; 2401 public bool $isUndocumentable; 2402 2403 /** 2404 * @param AttributeInfo[] $attributes 2405 * @param Name[] $extends 2406 * @param Name[] $implements 2407 * @param ConstInfo[] $constInfos 2408 * @param PropertyInfo[] $propertyInfos 2409 * @param FuncInfo[] $funcInfos 2410 * @param EnumCaseInfo[] $enumCaseInfos 2411 */ 2412 public function __construct( 2413 Name $name, 2414 int $flags, 2415 string $type, 2416 ?string $alias, 2417 ?SimpleType $enumBackingType, 2418 bool $isDeprecated, 2419 bool $isStrictProperties, 2420 array $attributes, 2421 bool $isNotSerializable, 2422 array $extends, 2423 array $implements, 2424 array $constInfos, 2425 array $propertyInfos, 2426 array $funcInfos, 2427 array $enumCaseInfos, 2428 ?string $cond, 2429 ?int $minimumPhpVersionIdCompatibility, 2430 bool $isUndocumentable 2431 ) { 2432 $this->name = $name; 2433 $this->flags = $flags; 2434 $this->type = $type; 2435 $this->alias = $alias; 2436 $this->enumBackingType = $enumBackingType; 2437 $this->isDeprecated = $isDeprecated; 2438 $this->isStrictProperties = $isStrictProperties; 2439 $this->attributes = $attributes; 2440 $this->isNotSerializable = $isNotSerializable; 2441 $this->extends = $extends; 2442 $this->implements = $implements; 2443 $this->constInfos = $constInfos; 2444 $this->propertyInfos = $propertyInfos; 2445 $this->funcInfos = $funcInfos; 2446 $this->enumCaseInfos = $enumCaseInfos; 2447 $this->cond = $cond; 2448 $this->phpVersionIdMinimumCompatibility = $minimumPhpVersionIdCompatibility; 2449 $this->isUndocumentable = $isUndocumentable; 2450 } 2451 2452 /** 2453 * @param ConstInfo[] $allConstInfos 2454 */ 2455 public function getRegistration(iterable $allConstInfos): string 2456 { 2457 $params = []; 2458 foreach ($this->extends as $extends) { 2459 $params[] = "zend_class_entry *class_entry_" . implode("_", $extends->parts); 2460 } 2461 foreach ($this->implements as $implements) { 2462 $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->parts); 2463 } 2464 2465 $escapedName = implode("_", $this->name->parts); 2466 2467 $code = ''; 2468 2469 $php81MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_81_VERSION_ID; 2470 $php82MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_82_VERSION_ID; 2471 2472 if ($this->type === "enum" && !$php81MinimumCompatibility) { 2473 $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n"; 2474 } 2475 2476 if ($this->cond) { 2477 $code .= "#if {$this->cond}\n"; 2478 } 2479 2480 $code .= "static zend_class_entry *register_class_$escapedName(" . (empty($params) ? "void" : implode(", ", $params)) . ")\n"; 2481 2482 $code .= "{\n"; 2483 if ($this->type === "enum") { 2484 $name = addslashes((string) $this->name); 2485 $backingType = $this->enumBackingType 2486 ? $this->enumBackingType->toTypeCode() : "IS_UNDEF"; 2487 $code .= "\tzend_class_entry *class_entry = zend_register_internal_enum(\"$name\", $backingType, class_{$escapedName}_methods);\n"; 2488 } else { 2489 $code .= "\tzend_class_entry ce, *class_entry;\n\n"; 2490 if (count($this->name->parts) > 1) { 2491 $className = $this->name->getLast(); 2492 $namespace = addslashes((string) $this->name->slice(0, -1)); 2493 2494 $code .= "\tINIT_NS_CLASS_ENTRY(ce, \"$namespace\", \"$className\", class_{$escapedName}_methods);\n"; 2495 } else { 2496 $code .= "\tINIT_CLASS_ENTRY(ce, \"$this->name\", class_{$escapedName}_methods);\n"; 2497 } 2498 2499 if ($this->type === "class" || $this->type === "trait") { 2500 $code .= "\tclass_entry = zend_register_internal_class_ex(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ");\n"; 2501 } else { 2502 $code .= "\tclass_entry = zend_register_internal_interface(&ce);\n"; 2503 } 2504 } 2505 2506 $flagCodes = generateVersionDependentFlagCode("\tclass_entry->ce_flags |= %s;\n", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility); 2507 $code .= implode("", $flagCodes); 2508 2509 $implements = array_map( 2510 function (Name $item) { 2511 return "class_entry_" . implode("_", $item->parts); 2512 }, 2513 $this->type === "interface" ? $this->extends : $this->implements 2514 ); 2515 2516 if (!empty($implements)) { 2517 $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n"; 2518 } 2519 2520 if ($this->alias) { 2521 $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n"; 2522 } 2523 2524 foreach ($this->constInfos as $const) { 2525 $code .= $const->getDeclaration($allConstInfos); 2526 } 2527 2528 foreach ($this->enumCaseInfos as $enumCase) { 2529 $code .= $enumCase->getDeclaration($allConstInfos); 2530 } 2531 2532 foreach ($this->propertyInfos as $property) { 2533 $code .= $property->getDeclaration($allConstInfos); 2534 } 2535 2536 if (!empty($this->attributes)) { 2537 if (!$php82MinimumCompatibility) { 2538 $code .= "\n#if (PHP_VERSION_ID >= " . PHP_82_VERSION_ID . ")"; 2539 } 2540 2541 foreach ($this->attributes as $attribute) { 2542 $code .= $attribute->generateCode("zend_add_class_attribute(class_entry", "class_$escapedName", $allConstInfos); 2543 } 2544 2545 if (!$php82MinimumCompatibility) { 2546 $code .= "#endif\n"; 2547 } 2548 } 2549 2550 if ($attributeInitializationCode = generateAttributeInitialization($this->funcInfos, $allConstInfos, $this->cond)) { 2551 if (!$php82MinimumCompatibility) { 2552 $code .= "#if (PHP_VERSION_ID >= " . PHP_82_VERSION_ID . ")\n"; 2553 } 2554 2555 $code .= "\n" . $attributeInitializationCode; 2556 2557 if (!$php82MinimumCompatibility) { 2558 $code .= "#endif\n"; 2559 } 2560 } 2561 2562 $code .= "\n\treturn class_entry;\n"; 2563 2564 $code .= "}\n"; 2565 2566 if ($this->cond) { 2567 $code .= "#endif\n"; 2568 } 2569 2570 if ($this->type === "enum" && !$php81MinimumCompatibility) { 2571 $code .= "#endif\n"; 2572 } 2573 2574 return $code; 2575 } 2576 2577 /** 2578 * @return array<int, string[]> 2579 */ 2580 private function getFlagsByPhpVersion(): array 2581 { 2582 $php70Flags = []; 2583 2584 if ($this->type === "trait") { 2585 $php70Flags[] = "ZEND_ACC_TRAIT"; 2586 } 2587 2588 if ($this->flags & Class_::MODIFIER_FINAL) { 2589 $php70Flags[] = "ZEND_ACC_FINAL"; 2590 } 2591 2592 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 2593 $php70Flags[] = "ZEND_ACC_ABSTRACT"; 2594 } 2595 2596 if ($this->isDeprecated) { 2597 $php70Flags[] = "ZEND_ACC_DEPRECATED"; 2598 } 2599 2600 $php80Flags = $php70Flags; 2601 2602 if ($this->isStrictProperties) { 2603 $php80Flags[] = "ZEND_ACC_NO_DYNAMIC_PROPERTIES"; 2604 } 2605 2606 $php81Flags = $php80Flags; 2607 2608 if ($this->isNotSerializable) { 2609 $php81Flags[] = "ZEND_ACC_NOT_SERIALIZABLE"; 2610 } 2611 2612 $php82Flags = $php81Flags; 2613 2614 if ($this->flags & Class_::MODIFIER_READONLY) { 2615 $php82Flags[] = "ZEND_ACC_READONLY_CLASS"; 2616 } 2617 2618 foreach ($this->attributes as $attr) { 2619 if ($attr->class === "AllowDynamicProperties") { 2620 $php82Flags[] = "ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES"; 2621 break; 2622 } 2623 } 2624 2625 return [ 2626 PHP_70_VERSION_ID => $php70Flags, 2627 PHP_80_VERSION_ID => $php80Flags, 2628 PHP_81_VERSION_ID => $php81Flags, 2629 PHP_82_VERSION_ID => $php82Flags, 2630 ]; 2631 } 2632 2633 /** 2634 * @param array<string, ClassInfo> $classMap 2635 * @param iterable<ConstInfo> $allConstInfos 2636 * @param iterable<ConstInfo> $allConstInfo 2637 */ 2638 public function getClassSynopsisDocument(array $classMap, iterable $allConstInfos): ?string { 2639 2640 $doc = new DOMDocument(); 2641 $doc->formatOutput = true; 2642 $classSynopsis = $this->getClassSynopsisElement($doc, $classMap, $allConstInfos); 2643 if (!$classSynopsis) { 2644 return null; 2645 } 2646 2647 $doc->appendChild($classSynopsis); 2648 2649 return $doc->saveXML(); 2650 } 2651 2652 /** 2653 * @param array<string, ClassInfo> $classMap 2654 * @param iterable<ConstInfo> $allConstInfos 2655 */ 2656 public function getClassSynopsisElement(DOMDocument $doc, array $classMap, iterable $allConstInfos): ?DOMElement { 2657 2658 $classSynopsis = $doc->createElement("classsynopsis"); 2659 $classSynopsis->appendChild(new DOMText("\n ")); 2660 2661 $ooElement = self::createOoElement($doc, $this, true, false, false, 4); 2662 if (!$ooElement) { 2663 return null; 2664 } 2665 $classSynopsis->appendChild($ooElement); 2666 $classSynopsis->appendChild(new DOMText("\n\n ")); 2667 2668 $classSynopsisInfo = $doc->createElement("classsynopsisinfo"); 2669 $classSynopsisInfo->appendChild(new DOMText("\n ")); 2670 $ooElement = self::createOoElement($doc, $this, false, true, false, 5); 2671 if (!$ooElement) { 2672 return null; 2673 } 2674 $classSynopsisInfo->appendChild($ooElement); 2675 2676 $classSynopsis->appendChild($classSynopsisInfo); 2677 2678 foreach ($this->extends as $k => $parent) { 2679 $parentInfo = $classMap[$parent->toString()] ?? null; 2680 if ($parentInfo === null) { 2681 throw new Exception("Missing parent class " . $parent->toString()); 2682 } 2683 2684 $ooElement = self::createOoElement( 2685 $doc, 2686 $parentInfo, 2687 $this->type === "interface", 2688 false, 2689 $k === 0, 2690 5 2691 ); 2692 if (!$ooElement) { 2693 return null; 2694 } 2695 2696 $classSynopsisInfo->appendChild(new DOMText("\n\n ")); 2697 $classSynopsisInfo->appendChild($ooElement); 2698 } 2699 2700 foreach ($this->implements as $interface) { 2701 $interfaceInfo = $classMap[$interface->toString()] ?? null; 2702 if (!$interfaceInfo) { 2703 throw new Exception("Missing implemented interface " . $interface->toString()); 2704 } 2705 2706 $ooElement = self::createOoElement($doc, $interfaceInfo, false, false, false, 5); 2707 if (!$ooElement) { 2708 return null; 2709 } 2710 $classSynopsisInfo->appendChild(new DOMText("\n\n ")); 2711 $classSynopsisInfo->appendChild($ooElement); 2712 } 2713 $classSynopsisInfo->appendChild(new DOMText("\n ")); 2714 2715 /** @var array<string, Name> $parentsWithInheritedConstants */ 2716 $parentsWithInheritedConstants = []; 2717 /** @var array<string, Name> $parentsWithInheritedProperties */ 2718 $parentsWithInheritedProperties = []; 2719 /** @var array<int, array{name: Name, types: int[]}> $parentsWithInheritedMethods */ 2720 $parentsWithInheritedMethods = []; 2721 2722 $this->collectInheritedMembers( 2723 $parentsWithInheritedConstants, 2724 $parentsWithInheritedProperties, 2725 $parentsWithInheritedMethods, 2726 $this->hasConstructor(), 2727 $classMap 2728 ); 2729 2730 $this->appendInheritedMemberSectionToClassSynopsis( 2731 $doc, 2732 $classSynopsis, 2733 $parentsWithInheritedConstants, 2734 "&Constants;", 2735 "&InheritedConstants;" 2736 ); 2737 2738 if (!empty($this->constInfos)) { 2739 $classSynopsis->appendChild(new DOMText("\n\n ")); 2740 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Constants;"); 2741 $classSynopsisInfo->setAttribute("role", "comment"); 2742 $classSynopsis->appendChild($classSynopsisInfo); 2743 2744 foreach ($this->constInfos as $constInfo) { 2745 $classSynopsis->appendChild(new DOMText("\n ")); 2746 $fieldSynopsisElement = $constInfo->getFieldSynopsisElement($doc, $allConstInfos); 2747 $classSynopsis->appendChild($fieldSynopsisElement); 2748 } 2749 } 2750 2751 if (!empty($this->propertyInfos)) { 2752 $classSynopsis->appendChild(new DOMText("\n\n ")); 2753 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;"); 2754 $classSynopsisInfo->setAttribute("role", "comment"); 2755 $classSynopsis->appendChild($classSynopsisInfo); 2756 2757 foreach ($this->propertyInfos as $propertyInfo) { 2758 $classSynopsis->appendChild(new DOMText("\n ")); 2759 $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc, $allConstInfos); 2760 $classSynopsis->appendChild($fieldSynopsisElement); 2761 } 2762 } 2763 2764 $this->appendInheritedMemberSectionToClassSynopsis( 2765 $doc, 2766 $classSynopsis, 2767 $parentsWithInheritedProperties, 2768 "&Properties;", 2769 "&InheritedProperties;" 2770 ); 2771 2772 if (!empty($this->funcInfos)) { 2773 $classSynopsis->appendChild(new DOMText("\n\n ")); 2774 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Methods;"); 2775 $classSynopsisInfo->setAttribute("role", "comment"); 2776 $classSynopsis->appendChild($classSynopsisInfo); 2777 2778 $classReference = self::getClassSynopsisReference($this->name); 2779 $escapedName = addslashes($this->name->__toString()); 2780 2781 if ($this->hasConstructor()) { 2782 $classSynopsis->appendChild(new DOMText("\n ")); 2783 $includeElement = $this->createIncludeElement( 2784 $doc, 2785 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[@role='$escapedName'])" 2786 ); 2787 $classSynopsis->appendChild($includeElement); 2788 } 2789 2790 if ($this->hasMethods()) { 2791 $classSynopsis->appendChild(new DOMText("\n ")); 2792 $includeElement = $this->createIncludeElement( 2793 $doc, 2794 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[@role='$escapedName'])" 2795 ); 2796 $classSynopsis->appendChild($includeElement); 2797 } 2798 2799 if ($this->hasDestructor()) { 2800 $classSynopsis->appendChild(new DOMText("\n ")); 2801 $includeElement = $this->createIncludeElement( 2802 $doc, 2803 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[@role='$escapedName'])" 2804 ); 2805 $classSynopsis->appendChild($includeElement); 2806 } 2807 } 2808 2809 if (!empty($parentsWithInheritedMethods)) { 2810 $classSynopsis->appendChild(new DOMText("\n\n ")); 2811 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&InheritedMethods;"); 2812 $classSynopsisInfo->setAttribute("role", "comment"); 2813 $classSynopsis->appendChild($classSynopsisInfo); 2814 2815 foreach ($parentsWithInheritedMethods as $parent) { 2816 $parentName = $parent["name"]; 2817 $parentMethodsynopsisTypes = $parent["types"]; 2818 2819 $parentReference = self::getClassSynopsisReference($parentName); 2820 $escapedParentName = addslashes($parentName->__toString()); 2821 2822 foreach ($parentMethodsynopsisTypes as $parentMethodsynopsisType) { 2823 $classSynopsis->appendChild(new DOMText("\n ")); 2824 $includeElement = $this->createIncludeElement( 2825 $doc, 2826 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$parentReference')/db:refentry/db:refsect1[@role='description']/descendant::db:{$parentMethodsynopsisType}[@role='$escapedParentName'])" 2827 ); 2828 2829 $classSynopsis->appendChild($includeElement); 2830 } 2831 } 2832 } 2833 2834 $classSynopsis->appendChild(new DOMText("\n ")); 2835 2836 return $classSynopsis; 2837 } 2838 2839 private static function createOoElement( 2840 DOMDocument $doc, 2841 ClassInfo $classInfo, 2842 bool $overrideToClass, 2843 bool $withModifiers, 2844 bool $isExtends, 2845 int $indentationLevel 2846 ): ?DOMElement { 2847 $indentation = str_repeat(" ", $indentationLevel); 2848 2849 if ($classInfo->type !== "class" && $classInfo->type !== "interface") { 2850 echo "Class synopsis generation is not implemented for " . $classInfo->type . "\n"; 2851 return null; 2852 } 2853 2854 $type = $overrideToClass ? "class" : $classInfo->type; 2855 2856 $ooElement = $doc->createElement("oo$type"); 2857 $ooElement->appendChild(new DOMText("\n$indentation ")); 2858 if ($isExtends) { 2859 $ooElement->appendChild($doc->createElement('modifier', 'extends')); 2860 $ooElement->appendChild(new DOMText("\n$indentation ")); 2861 } elseif ($withModifiers) { 2862 if ($classInfo->flags & Class_::MODIFIER_FINAL) { 2863 $ooElement->appendChild($doc->createElement('modifier', 'final')); 2864 $ooElement->appendChild(new DOMText("\n$indentation ")); 2865 } 2866 if ($classInfo->flags & Class_::MODIFIER_ABSTRACT) { 2867 $ooElement->appendChild($doc->createElement('modifier', 'abstract')); 2868 $ooElement->appendChild(new DOMText("\n$indentation ")); 2869 } 2870 } 2871 2872 $nameElement = $doc->createElement("{$type}name", $classInfo->name->toString()); 2873 $ooElement->appendChild($nameElement); 2874 $ooElement->appendChild(new DOMText("\n$indentation")); 2875 2876 return $ooElement; 2877 } 2878 2879 public static function getClassSynopsisFilename(Name $name): string { 2880 return strtolower(str_replace("_", "-", implode('-', $name->parts))); 2881 } 2882 2883 public static function getClassSynopsisReference(Name $name): string { 2884 return "class." . self::getClassSynopsisFilename($name); 2885 } 2886 2887 /** 2888 * @param array<string, Name> $parentsWithInheritedConstants 2889 * @param array<string, Name> $parentsWithInheritedProperties 2890 * @param array<string, array{name: Name, types: int[]}> $parentsWithInheritedMethods 2891 * @param array<string, ClassInfo> $classMap 2892 */ 2893 private function collectInheritedMembers( 2894 array &$parentsWithInheritedConstants, 2895 array &$parentsWithInheritedProperties, 2896 array &$parentsWithInheritedMethods, 2897 bool $hasConstructor, 2898 array $classMap 2899 ): void { 2900 foreach ($this->extends as $parent) { 2901 $parentInfo = $classMap[$parent->toString()] ?? null; 2902 $parentName = $parent->toString(); 2903 2904 if (!$parentInfo) { 2905 throw new Exception("Missing parent class $parentName"); 2906 } 2907 2908 if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parentName])) { 2909 $parentsWithInheritedConstants[] = $parent; 2910 } 2911 2912 if (!empty($parentInfo->propertyInfos) && !isset($parentsWithInheritedProperties[$parentName])) { 2913 $parentsWithInheritedProperties[$parentName] = $parent; 2914 } 2915 2916 if (!$hasConstructor && $parentInfo->hasNonPrivateConstructor()) { 2917 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 2918 $parentsWithInheritedMethods[$parentName]["types"][] = "constructorsynopsis"; 2919 } 2920 2921 if ($parentInfo->hasMethods()) { 2922 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 2923 $parentsWithInheritedMethods[$parentName]["types"][] = "methodsynopsis"; 2924 } 2925 2926 if ($parentInfo->hasDestructor()) { 2927 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 2928 $parentsWithInheritedMethods[$parentName]["types"][] = "destructorsynopsis"; 2929 } 2930 2931 $parentInfo->collectInheritedMembers( 2932 $parentsWithInheritedConstants, 2933 $parentsWithInheritedProperties, 2934 $parentsWithInheritedMethods, 2935 $hasConstructor, 2936 $classMap 2937 ); 2938 } 2939 2940 foreach ($this->implements as $parent) { 2941 $parentInfo = $classMap[$parent->toString()] ?? null; 2942 if (!$parentInfo) { 2943 throw new Exception("Missing parent interface " . $parent->toString()); 2944 } 2945 2946 if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parent->toString()])) { 2947 $parentsWithInheritedConstants[$parent->toString()] = $parent; 2948 } 2949 2950 $unusedParentsWithInheritedProperties = []; 2951 $unusedParentsWithInheritedMethods = []; 2952 2953 $parentInfo->collectInheritedMembers( 2954 $parentsWithInheritedConstants, 2955 $unusedParentsWithInheritedProperties, 2956 $unusedParentsWithInheritedMethods, 2957 $hasConstructor, 2958 $classMap 2959 ); 2960 } 2961 } 2962 2963 private function hasConstructor(): bool 2964 { 2965 foreach ($this->funcInfos as $funcInfo) { 2966 if ($funcInfo->name->isConstructor()) { 2967 return true; 2968 } 2969 } 2970 2971 return false; 2972 } 2973 2974 private function hasNonPrivateConstructor(): bool 2975 { 2976 foreach ($this->funcInfos as $funcInfo) { 2977 if ($funcInfo->name->isConstructor() && !($funcInfo->flags & Class_::MODIFIER_PRIVATE)) { 2978 return true; 2979 } 2980 } 2981 2982 return false; 2983 } 2984 2985 private function hasDestructor(): bool 2986 { 2987 foreach ($this->funcInfos as $funcInfo) { 2988 if ($funcInfo->name->isDestructor()) { 2989 return true; 2990 } 2991 } 2992 2993 return false; 2994 } 2995 2996 private function hasMethods(): bool 2997 { 2998 foreach ($this->funcInfos as $funcInfo) { 2999 if (!$funcInfo->name->isConstructor() && !$funcInfo->name->isDestructor()) { 3000 return true; 3001 } 3002 } 3003 3004 return false; 3005 } 3006 3007 private function createIncludeElement(DOMDocument $doc, string $query): DOMElement 3008 { 3009 $includeElement = $doc->createElement("xi:include"); 3010 $attr = $doc->createAttribute("xpointer"); 3011 $attr->value = $query; 3012 $includeElement->appendChild($attr); 3013 $fallbackElement = $doc->createElement("xi:fallback"); 3014 $includeElement->appendChild(new DOMText("\n ")); 3015 $includeElement->appendChild($fallbackElement); 3016 $includeElement->appendChild(new DOMText("\n ")); 3017 3018 return $includeElement; 3019 } 3020 3021 public function __clone() 3022 { 3023 foreach ($this->propertyInfos as $key => $propertyInfo) { 3024 $this->propertyInfos[$key] = clone $propertyInfo; 3025 } 3026 3027 foreach ($this->funcInfos as $key => $funcInfo) { 3028 $this->funcInfos[$key] = clone $funcInfo; 3029 } 3030 } 3031 3032 /** 3033 * @param Name[] $parents 3034 */ 3035 private function appendInheritedMemberSectionToClassSynopsis(DOMDocument $doc, DOMElement $classSynopsis, array $parents, string $label, string $inheritedLabel): void 3036 { 3037 if (empty($parents)) { 3038 return; 3039 } 3040 3041 $classSynopsis->appendChild(new DOMText("\n\n ")); 3042 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "$inheritedLabel"); 3043 $classSynopsisInfo->setAttribute("role", "comment"); 3044 $classSynopsis->appendChild($classSynopsisInfo); 3045 3046 foreach ($parents as $parent) { 3047 $classSynopsis->appendChild(new DOMText("\n ")); 3048 $parentReference = self::getClassSynopsisReference($parent); 3049 3050 $includeElement = $this->createIncludeElement( 3051 $doc, 3052 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$parentReference')/db:partintro/db:section/db:classsynopsis/db:fieldsynopsis[preceding-sibling::db:classsynopsisinfo[1][@role='comment' and text()='$label']]))" 3053 ); 3054 $classSynopsis->appendChild($includeElement); 3055 } 3056 } 3057} 3058 3059class FileInfo { 3060 /** @var string[] */ 3061 public array $dependencies = []; 3062 /** @var ConstInfo[] */ 3063 public array $constInfos = []; 3064 /** @var FuncInfo[] */ 3065 public array $funcInfos = []; 3066 /** @var ClassInfo[] */ 3067 public array $classInfos = []; 3068 public bool $generateFunctionEntries = false; 3069 public string $declarationPrefix = ""; 3070 public ?int $generateLegacyArginfoForPhpVersionId = null; 3071 public bool $generateClassEntries = false; 3072 public bool $isUndocumentable = false; 3073 3074 /** 3075 * @return iterable<FuncInfo> 3076 */ 3077 public function getAllFuncInfos(): iterable { 3078 yield from $this->funcInfos; 3079 foreach ($this->classInfos as $classInfo) { 3080 yield from $classInfo->funcInfos; 3081 } 3082 } 3083 3084 /** 3085 * @return iterable<ConstInfo> 3086 */ 3087 public function getAllConstInfos(): iterable { 3088 $result = $this->constInfos; 3089 3090 foreach ($this->classInfos as $classInfo) { 3091 $result = array_merge($result, $classInfo->constInfos); 3092 } 3093 3094 return $result; 3095 } 3096 3097 /** 3098 * @return iterable<PropertyInfo> 3099 */ 3100 public function getAllPropertyInfos(): iterable { 3101 foreach ($this->classInfos as $classInfo) { 3102 yield from $classInfo->propertyInfos; 3103 } 3104 } 3105 3106 public function __clone() 3107 { 3108 foreach ($this->funcInfos as $key => $funcInfo) { 3109 $this->funcInfos[$key] = clone $funcInfo; 3110 } 3111 3112 foreach ($this->classInfos as $key => $classInfo) { 3113 $this->classInfos[$key] = clone $classInfo; 3114 } 3115 } 3116} 3117 3118class DocCommentTag { 3119 public string $name; 3120 public ?string $value; 3121 3122 public function __construct(string $name, ?string $value) { 3123 $this->name = $name; 3124 $this->value = $value; 3125 } 3126 3127 public function getValue(): string { 3128 if ($this->value === null) { 3129 throw new Exception("@$this->name does not have a value"); 3130 } 3131 3132 return $this->value; 3133 } 3134 3135 public function getType(): string { 3136 $value = $this->getValue(); 3137 3138 $matches = []; 3139 3140 if ($this->name === "param") { 3141 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)\s*\$\w+.*$/', $value, $matches); 3142 } elseif ($this->name === "return" || $this->name === "var") { 3143 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)/', $value, $matches); 3144 } 3145 3146 if (!isset($matches[1])) { 3147 throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\""); 3148 } 3149 3150 return trim($matches[1]); 3151 } 3152 3153 public function getVariableName(): string { 3154 $value = $this->value; 3155 if ($value === null || strlen($value) === 0) { 3156 throw new Exception("@$this->name doesn't have any value"); 3157 } 3158 3159 $matches = []; 3160 3161 if ($this->name === "param") { 3162 preg_match('/^\s*[\w\|\\\\\[\]]+\s*\$(\w+).*$/', $value, $matches); 3163 } elseif ($this->name === "prefer-ref") { 3164 preg_match('/^\s*\$(\w+).*$/', $value, $matches); 3165 } 3166 3167 if (!isset($matches[1])) { 3168 throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\""); 3169 } 3170 3171 return $matches[1]; 3172 } 3173} 3174 3175/** @return DocCommentTag[] */ 3176function parseDocComment(DocComment $comment): array { 3177 $commentText = substr($comment->getText(), 2, -2); 3178 $tags = []; 3179 foreach (explode("\n", $commentText) as $commentLine) { 3180 $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/'; 3181 if (preg_match($regex, trim($commentLine), $matches)) { 3182 $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null); 3183 } 3184 } 3185 3186 return $tags; 3187} 3188 3189function parseFunctionLike( 3190 PrettyPrinterAbstract $prettyPrinter, 3191 FunctionOrMethodName $name, 3192 int $classFlags, 3193 int $flags, 3194 Node\FunctionLike $func, 3195 ?string $cond, 3196 bool $isUndocumentable 3197): FuncInfo { 3198 try { 3199 $comment = $func->getDocComment(); 3200 $paramMeta = []; 3201 $aliasType = null; 3202 $alias = null; 3203 $isDeprecated = false; 3204 $supportsCompileTimeEval = false; 3205 $verify = true; 3206 $docReturnType = null; 3207 $tentativeReturnType = false; 3208 $docParamTypes = []; 3209 $refcount = null; 3210 3211 if ($comment) { 3212 $tags = parseDocComment($comment); 3213 foreach ($tags as $tag) { 3214 switch ($tag->name) { 3215 case 'alias': 3216 case 'implementation-alias': 3217 $aliasType = $tag->name; 3218 $aliasParts = explode("::", $tag->getValue()); 3219 if (count($aliasParts) === 1) { 3220 $alias = new FunctionName(new Name($aliasParts[0])); 3221 } else { 3222 $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]); 3223 } 3224 break; 3225 3226 case 'deprecated': 3227 $isDeprecated = true; 3228 break; 3229 3230 case 'no-verify': 3231 $verify = false; 3232 break; 3233 3234 case 'tentative-return-type': 3235 $tentativeReturnType = true; 3236 break; 3237 3238 case 'return': 3239 $docReturnType = $tag->getType(); 3240 break; 3241 3242 case 'param': 3243 $docParamTypes[$tag->getVariableName()] = $tag->getType(); 3244 break; 3245 3246 case 'refcount': 3247 $refcount = $tag->getValue(); 3248 break; 3249 3250 case 'compile-time-eval': 3251 $supportsCompileTimeEval = true; 3252 break; 3253 3254 case 'prefer-ref': 3255 $varName = $tag->getVariableName(); 3256 if (!isset($paramMeta[$varName])) { 3257 $paramMeta[$varName] = []; 3258 } 3259 $paramMeta[$varName][$tag->name] = true; 3260 break; 3261 3262 case 'undocumentable': 3263 $isUndocumentable = true; 3264 break; 3265 } 3266 } 3267 } 3268 3269 $varNameSet = []; 3270 $args = []; 3271 $numRequiredArgs = 0; 3272 $foundVariadic = false; 3273 foreach ($func->getParams() as $i => $param) { 3274 $varName = $param->var->name; 3275 $preferRef = !empty($paramMeta[$varName]['prefer-ref']); 3276 $attributes = []; 3277 foreach ($param->attrGroups as $attrGroup) { 3278 foreach ($attrGroup->attrs as $attr) { 3279 $attributes[] = new AttributeInfo($attr->name->toString(), $attr->args); 3280 } 3281 } 3282 unset($paramMeta[$varName]); 3283 3284 if (isset($varNameSet[$varName])) { 3285 throw new Exception("Duplicate parameter name $varName"); 3286 } 3287 $varNameSet[$varName] = true; 3288 3289 if ($preferRef) { 3290 $sendBy = ArgInfo::SEND_PREFER_REF; 3291 } else if ($param->byRef) { 3292 $sendBy = ArgInfo::SEND_BY_REF; 3293 } else { 3294 $sendBy = ArgInfo::SEND_BY_VAL; 3295 } 3296 3297 if ($foundVariadic) { 3298 throw new Exception("Only the last parameter can be variadic"); 3299 } 3300 3301 $type = $param->type ? Type::fromNode($param->type) : null; 3302 if ($type === null && !isset($docParamTypes[$varName])) { 3303 throw new Exception("Missing parameter type"); 3304 } 3305 3306 if ($param->default instanceof Expr\ConstFetch && 3307 $param->default->name->toLowerString() === "null" && 3308 $type && !$type->isNullable() 3309 ) { 3310 $simpleType = $type->tryToSimpleType(); 3311 if ($simpleType === null) { 3312 throw new Exception("Parameter $varName has null default, but is not nullable"); 3313 } 3314 } 3315 3316 if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { 3317 throw new Exception('The exact class name must be used instead of "self"'); 3318 } 3319 3320 $foundVariadic = $param->variadic; 3321 3322 $args[] = new ArgInfo( 3323 $varName, 3324 $sendBy, 3325 $param->variadic, 3326 $type, 3327 isset($docParamTypes[$varName]) ? Type::fromString($docParamTypes[$varName]) : null, 3328 $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null, 3329 $attributes 3330 ); 3331 if (!$param->default && !$param->variadic) { 3332 $numRequiredArgs = $i + 1; 3333 } 3334 } 3335 3336 foreach (array_keys($paramMeta) as $var) { 3337 throw new Exception("Found metadata for invalid param $var"); 3338 } 3339 3340 $returnType = $func->getReturnType(); 3341 if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { 3342 throw new Exception("Missing return type"); 3343 } 3344 3345 $return = new ReturnInfo( 3346 $func->returnsByRef(), 3347 $returnType ? Type::fromNode($returnType) : null, 3348 $docReturnType ? Type::fromString($docReturnType) : null, 3349 $tentativeReturnType, 3350 $refcount 3351 ); 3352 3353 return new FuncInfo( 3354 $name, 3355 $classFlags, 3356 $flags, 3357 $aliasType, 3358 $alias, 3359 $isDeprecated, 3360 $supportsCompileTimeEval, 3361 $verify, 3362 $args, 3363 $return, 3364 $numRequiredArgs, 3365 $cond, 3366 $isUndocumentable 3367 ); 3368 } catch (Exception $e) { 3369 throw new Exception($name . "(): " .$e->getMessage()); 3370 } 3371} 3372 3373function parseConstLike( 3374 PrettyPrinterAbstract $prettyPrinter, 3375 ConstOrClassConstName $name, 3376 Node\Const_ $const, 3377 int $flags, 3378 ?DocComment $docComment, 3379 ?string $cond, 3380 ?int $phpVersionIdMinimumCompatibility 3381): ConstInfo { 3382 $phpDocType = null; 3383 $deprecated = false; 3384 $cValue = null; 3385 $link = null; 3386 if ($docComment) { 3387 $tags = parseDocComment($docComment); 3388 foreach ($tags as $tag) { 3389 if ($tag->name === 'var') { 3390 $phpDocType = $tag->getType(); 3391 } elseif ($tag->name === 'deprecated') { 3392 $deprecated = true; 3393 } elseif ($tag->name === 'cvalue') { 3394 $cValue = $tag->value; 3395 } elseif ($tag->name === 'link') { 3396 $link = $tag->value; 3397 } 3398 } 3399 } 3400 3401 if ($phpDocType === null) { 3402 throw new Exception("Missing type for constant " . $name->__toString()); 3403 } 3404 3405 return new ConstInfo( 3406 $name, 3407 $flags, 3408 $const->value, 3409 $prettyPrinter->prettyPrintExpr($const->value), 3410 Type::fromString($phpDocType), 3411 $deprecated, 3412 $cond, 3413 $cValue, 3414 $link, 3415 $phpVersionIdMinimumCompatibility 3416 ); 3417} 3418 3419function parseProperty( 3420 Name $class, 3421 int $flags, 3422 Stmt\PropertyProperty $property, 3423 ?Node $type, 3424 ?DocComment $comment, 3425 PrettyPrinterAbstract $prettyPrinter, 3426 ?int $phpVersionIdMinimumCompatibility 3427): PropertyInfo { 3428 $phpDocType = null; 3429 $isDocReadonly = false; 3430 $link = null; 3431 3432 if ($comment) { 3433 $tags = parseDocComment($comment); 3434 foreach ($tags as $tag) { 3435 if ($tag->name === 'var') { 3436 $phpDocType = $tag->getType(); 3437 } elseif ($tag->name === 'readonly') { 3438 $isDocReadonly = true; 3439 } elseif ($tag->name === 'link') { 3440 $link = $tag->value; 3441 } 3442 } 3443 } 3444 3445 $propertyType = $type ? Type::fromNode($type) : null; 3446 if ($propertyType === null && !$phpDocType) { 3447 throw new Exception("Missing type for property $class::\$$property->name"); 3448 } 3449 3450 if ($property->default instanceof Expr\ConstFetch && 3451 $property->default->name->toLowerString() === "null" && 3452 $propertyType && !$propertyType->isNullable() 3453 ) { 3454 $simpleType = $propertyType->tryToSimpleType(); 3455 if ($simpleType === null) { 3456 throw new Exception( 3457 "Property $class::\$$property->name has null default, but is not nullable"); 3458 } 3459 } 3460 3461 return new PropertyInfo( 3462 new PropertyName($class, $property->name->__toString()), 3463 $flags, 3464 $propertyType, 3465 $phpDocType ? Type::fromString($phpDocType) : null, 3466 $property->default, 3467 $property->default ? $prettyPrinter->prettyPrintExpr($property->default) : null, 3468 $isDocReadonly, 3469 $link, 3470 $phpVersionIdMinimumCompatibility 3471 ); 3472} 3473 3474/** 3475 * @param ConstInfo[] $consts 3476 * @param PropertyInfo[] $properties 3477 * @param FuncInfo[] $methods 3478 * @param EnumCaseInfo[] $enumCases 3479 */ 3480function parseClass( 3481 Name $name, 3482 Stmt\ClassLike $class, 3483 array $consts, 3484 array $properties, 3485 array $methods, 3486 array $enumCases, 3487 ?string $cond, 3488 ?int $minimumPhpVersionIdCompatibility, 3489 bool $isUndocumentable 3490): ClassInfo { 3491 $flags = $class instanceof Class_ ? $class->flags : 0; 3492 $comment = $class->getDocComment(); 3493 $alias = null; 3494 $isDeprecated = false; 3495 $isStrictProperties = false; 3496 $isNotSerializable = false; 3497 $allowsDynamicProperties = false; 3498 $attributes = []; 3499 3500 if ($comment) { 3501 $tags = parseDocComment($comment); 3502 foreach ($tags as $tag) { 3503 if ($tag->name === 'alias') { 3504 $alias = $tag->getValue(); 3505 } else if ($tag->name === 'deprecated') { 3506 $isDeprecated = true; 3507 } else if ($tag->name === 'strict-properties') { 3508 $isStrictProperties = true; 3509 } else if ($tag->name === 'not-serializable') { 3510 $isNotSerializable = true; 3511 } else if ($tag->name === 'undocumentable') { 3512 $isUndocumentable = true; 3513 } 3514 } 3515 } 3516 3517 foreach ($class->attrGroups as $attrGroup) { 3518 foreach ($attrGroup->attrs as $attr) { 3519 $attributes[] = new AttributeInfo($attr->name->toString(), $attr->args); 3520 switch ($attr->name->toString()) { 3521 case 'AllowDynamicProperties': 3522 $allowsDynamicProperties = true; 3523 break; 3524 } 3525 } 3526 } 3527 3528 if ($isStrictProperties && $allowsDynamicProperties) { 3529 throw new Exception("A class may not have '@strict-properties' and '#[\\AllowDynamicProperties]' at the same time."); 3530 } 3531 3532 $extends = []; 3533 $implements = []; 3534 3535 if ($class instanceof Class_) { 3536 $classKind = "class"; 3537 if ($class->extends) { 3538 $extends[] = $class->extends; 3539 } 3540 $implements = $class->implements; 3541 } elseif ($class instanceof Interface_) { 3542 $classKind = "interface"; 3543 $extends = $class->extends; 3544 } else if ($class instanceof Trait_) { 3545 $classKind = "trait"; 3546 } else if ($class instanceof Enum_) { 3547 $classKind = "enum"; 3548 $implements = $class->implements; 3549 } else { 3550 throw new Exception("Unknown class kind " . get_class($class)); 3551 } 3552 3553 if ($isUndocumentable) { 3554 foreach ($methods as $method) { 3555 $method->isUndocumentable = true; 3556 } 3557 } 3558 3559 return new ClassInfo( 3560 $name, 3561 $flags, 3562 $classKind, 3563 $alias, 3564 $class instanceof Enum_ && $class->scalarType !== null 3565 ? SimpleType::fromNode($class->scalarType) : null, 3566 $isDeprecated, 3567 $isStrictProperties, 3568 $attributes, 3569 $isNotSerializable, 3570 $extends, 3571 $implements, 3572 $consts, 3573 $properties, 3574 $methods, 3575 $enumCases, 3576 $cond, 3577 $minimumPhpVersionIdCompatibility, 3578 $isUndocumentable 3579 ); 3580} 3581 3582function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string { 3583 foreach ($stmt->getComments() as $comment) { 3584 $text = trim($comment->getText()); 3585 if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) { 3586 $conds[] = $matches[1]; 3587 } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) { 3588 $conds[] = "defined($matches[1])"; 3589 } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) { 3590 $conds[] = "!defined($matches[1])"; 3591 } else if (preg_match('/^#\s*else$/', $text)) { 3592 if (empty($conds)) { 3593 throw new Exception("Encountered else without corresponding #if"); 3594 } 3595 $cond = array_pop($conds); 3596 $conds[] = "!($cond)"; 3597 } else if (preg_match('/^#\s*endif$/', $text)) { 3598 if (empty($conds)) { 3599 throw new Exception("Encountered #endif without corresponding #if"); 3600 } 3601 array_pop($conds); 3602 } else if ($text[0] === '#') { 3603 throw new Exception("Unrecognized preprocessor directive \"$text\""); 3604 } 3605 } 3606 3607 return empty($conds) ? null : implode(' && ', $conds); 3608} 3609 3610function getFileDocComment(array $stmts): ?DocComment { 3611 if (empty($stmts)) { 3612 return null; 3613 } 3614 3615 $comments = $stmts[0]->getComments(); 3616 if (empty($comments)) { 3617 return null; 3618 } 3619 3620 if ($comments[0] instanceof DocComment) { 3621 return $comments[0]; 3622 } 3623 3624 return null; 3625} 3626 3627function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) { 3628 $conds = []; 3629 foreach ($stmts as $stmt) { 3630 if ($stmt instanceof Stmt\Nop) { 3631 continue; 3632 } 3633 3634 if ($stmt instanceof Stmt\Namespace_) { 3635 handleStatements($fileInfo, $stmt->stmts, $prettyPrinter); 3636 continue; 3637 } 3638 3639 $cond = handlePreprocessorConditions($conds, $stmt); 3640 3641 if ($stmt instanceof Stmt\Const_) { 3642 foreach ($stmt->consts as $const) { 3643 $fileInfo->constInfos[] = parseConstLike( 3644 $prettyPrinter, 3645 new ConstName($const->namespacedName, $const->name->toString()), 3646 $const, 3647 0, 3648 $stmt->getDocComment(), 3649 $cond, 3650 $fileInfo->generateLegacyArginfoForPhpVersionId 3651 ); 3652 } 3653 continue; 3654 } 3655 3656 if ($stmt instanceof Stmt\Function_) { 3657 $fileInfo->funcInfos[] = parseFunctionLike( 3658 $prettyPrinter, 3659 new FunctionName($stmt->namespacedName), 3660 0, 3661 0, 3662 $stmt, 3663 $cond, 3664 $fileInfo->isUndocumentable 3665 ); 3666 continue; 3667 } 3668 3669 if ($stmt instanceof Stmt\ClassLike) { 3670 $className = $stmt->namespacedName; 3671 $constInfos = []; 3672 $propertyInfos = []; 3673 $methodInfos = []; 3674 $enumCaseInfos = []; 3675 foreach ($stmt->stmts as $classStmt) { 3676 $cond = handlePreprocessorConditions($conds, $classStmt); 3677 if ($classStmt instanceof Stmt\Nop) { 3678 continue; 3679 } 3680 3681 $classFlags = $stmt instanceof Class_ ? $stmt->flags : 0; 3682 $abstractFlag = $stmt instanceof Stmt\Interface_ ? Class_::MODIFIER_ABSTRACT : 0; 3683 3684 if ($classStmt instanceof Stmt\ClassConst) { 3685 foreach ($classStmt->consts as $const) { 3686 $constInfos[] = parseConstLike( 3687 $prettyPrinter, 3688 new ClassConstName($className, $const->name->toString()), 3689 $const, 3690 $classStmt->flags, 3691 $classStmt->getDocComment(), 3692 $cond, 3693 $fileInfo->generateLegacyArginfoForPhpVersionId 3694 ); 3695 } 3696 } else if ($classStmt instanceof Stmt\Property) { 3697 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 3698 throw new Exception("Visibility modifier is required"); 3699 } 3700 foreach ($classStmt->props as $property) { 3701 $propertyInfos[] = parseProperty( 3702 $className, 3703 $classStmt->flags, 3704 $property, 3705 $classStmt->type, 3706 $classStmt->getDocComment(), 3707 $prettyPrinter, 3708 $fileInfo->generateLegacyArginfoForPhpVersionId 3709 ); 3710 } 3711 } else if ($classStmt instanceof Stmt\ClassMethod) { 3712 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 3713 throw new Exception("Visibility modifier is required"); 3714 } 3715 $methodInfos[] = parseFunctionLike( 3716 $prettyPrinter, 3717 new MethodName($className, $classStmt->name->toString()), 3718 $classFlags, 3719 $classStmt->flags | $abstractFlag, 3720 $classStmt, 3721 $cond, 3722 $fileInfo->isUndocumentable 3723 ); 3724 } else if ($classStmt instanceof Stmt\EnumCase) { 3725 $enumCaseInfos[] = new EnumCaseInfo( 3726 $classStmt->name->toString(), $classStmt->expr); 3727 } else { 3728 throw new Exception("Not implemented {$classStmt->getType()}"); 3729 } 3730 } 3731 3732 $fileInfo->classInfos[] = parseClass( 3733 $className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->generateLegacyArginfoForPhpVersionId, $fileInfo->isUndocumentable 3734 ); 3735 continue; 3736 } 3737 3738 if ($stmt instanceof Stmt\Expression) { 3739 $expr = $stmt->expr; 3740 if ($expr instanceof Expr\Include_) { 3741 $fileInfo->dependencies[] = (string)EvaluatedValue::createFromExpression($expr->expr, null, null, [])->value; 3742 continue; 3743 } 3744 } 3745 3746 throw new Exception("Unexpected node {$stmt->getType()}"); 3747 } 3748} 3749 3750function parseStubFile(string $code): FileInfo { 3751 $lexer = new PhpParser\Lexer\Emulative(); 3752 $parser = new PhpParser\Parser\Php7($lexer); 3753 $nodeTraverser = new PhpParser\NodeTraverser; 3754 $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); 3755 $prettyPrinter = new class extends Standard { 3756 protected function pName_FullyQualified(Name\FullyQualified $node) { 3757 return implode('\\', $node->parts); 3758 } 3759 }; 3760 3761 $stmts = $parser->parse($code); 3762 $nodeTraverser->traverse($stmts); 3763 3764 $fileInfo = new FileInfo; 3765 $fileDocComment = getFileDocComment($stmts); 3766 if ($fileDocComment) { 3767 $fileTags = parseDocComment($fileDocComment); 3768 foreach ($fileTags as $tag) { 3769 if ($tag->name === 'generate-function-entries') { 3770 $fileInfo->generateFunctionEntries = true; 3771 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 3772 } else if ($tag->name === 'generate-legacy-arginfo') { 3773 if ($tag->value && !in_array((int) $tag->value, ALL_PHP_VERSION_IDS, true)) { 3774 throw new Exception( 3775 "Legacy PHP version must be one of: \"" . PHP_70_VERSION_ID . "\" (PHP 7.0), \"" . PHP_80_VERSION_ID . "\" (PHP 8.0), " . 3776 "\"" . PHP_81_VERSION_ID . "\" (PHP 8.1), \"" . PHP_82_VERSION_ID . "\" (PHP 8.2), \"" . $tag->value . "\" provided" 3777 ); 3778 } 3779 3780 $fileInfo->generateLegacyArginfoForPhpVersionId = $tag->value ? (int) $tag->value : PHP_70_VERSION_ID; 3781 } else if ($tag->name === 'generate-class-entries') { 3782 $fileInfo->generateClassEntries = true; 3783 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 3784 } else if ($tag->name === 'undocumentable') { 3785 $fileInfo->isUndocumentable = true; 3786 } 3787 } 3788 } 3789 3790 // Generating class entries require generating function/method entries 3791 if ($fileInfo->generateClassEntries && !$fileInfo->generateFunctionEntries) { 3792 $fileInfo->generateFunctionEntries = true; 3793 } 3794 3795 handleStatements($fileInfo, $stmts, $prettyPrinter); 3796 return $fileInfo; 3797} 3798 3799function funcInfoToCode(FileInfo $fileInfo, FuncInfo $funcInfo): string { 3800 $code = ''; 3801 $returnType = $funcInfo->return->type; 3802 $isTentativeReturnType = $funcInfo->return->tentativeReturnType; 3803 $php81MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_81_VERSION_ID; 3804 3805 if ($returnType !== null) { 3806 if ($isTentativeReturnType && !$php81MinimumCompatibility) { 3807 $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n"; 3808 } 3809 if (null !== $simpleReturnType = $returnType->tryToSimpleType()) { 3810 if ($simpleReturnType->isBuiltin) { 3811 $code .= sprintf( 3812 "%s(%s, %d, %d, %s, %d)\n", 3813 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX", 3814 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 3815 $funcInfo->numRequiredArgs, 3816 $simpleReturnType->toTypeCode(), $returnType->isNullable() 3817 ); 3818 } else { 3819 $code .= sprintf( 3820 "%s(%s, %d, %d, %s, %d)\n", 3821 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX", 3822 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 3823 $funcInfo->numRequiredArgs, 3824 $simpleReturnType->toEscapedName(), $returnType->isNullable() 3825 ); 3826 } 3827 } else { 3828 $arginfoType = $returnType->toArginfoType(); 3829 if ($arginfoType->hasClassType()) { 3830 $code .= sprintf( 3831 "%s(%s, %d, %d, %s, %s)\n", 3832 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX", 3833 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 3834 $funcInfo->numRequiredArgs, 3835 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask() 3836 ); 3837 } else { 3838 $code .= sprintf( 3839 "%s(%s, %d, %d, %s)\n", 3840 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX", 3841 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 3842 $funcInfo->numRequiredArgs, 3843 $arginfoType->toTypeMask() 3844 ); 3845 } 3846 } 3847 if ($isTentativeReturnType && !$php81MinimumCompatibility) { 3848 $code .= sprintf( 3849 "#else\nZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n#endif\n", 3850 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 3851 ); 3852 } 3853 } else { 3854 $code .= sprintf( 3855 "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", 3856 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 3857 ); 3858 } 3859 3860 foreach ($funcInfo->args as $argInfo) { 3861 $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG"; 3862 $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; 3863 $argType = $argInfo->type; 3864 if ($argType !== null) { 3865 if (null !== $simpleArgType = $argType->tryToSimpleType()) { 3866 if ($simpleArgType->isBuiltin) { 3867 $code .= sprintf( 3868 "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n", 3869 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 3870 $simpleArgType->toTypeCode(), $argType->isNullable(), 3871 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 3872 ); 3873 } else { 3874 $code .= sprintf( 3875 "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n", 3876 $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 3877 $simpleArgType->toEscapedName(), $argType->isNullable(), 3878 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 3879 ); 3880 } 3881 } else { 3882 $arginfoType = $argType->toArginfoType(); 3883 if ($arginfoType->hasClassType()) { 3884 $code .= sprintf( 3885 "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s%s)\n", 3886 $argKind, $argInfo->getSendByString(), $argInfo->name, 3887 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(), 3888 !$argInfo->isVariadic ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 3889 ); 3890 } else { 3891 $code .= sprintf( 3892 "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n", 3893 $argKind, $argInfo->getSendByString(), $argInfo->name, 3894 $arginfoType->toTypeMask(), 3895 $argInfo->getDefaultValueAsArginfoString() 3896 ); 3897 } 3898 } 3899 } else { 3900 $code .= sprintf( 3901 "\tZEND_%s_INFO%s(%s, %s%s)\n", 3902 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 3903 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 3904 ); 3905 } 3906 } 3907 3908 $code .= "ZEND_END_ARG_INFO()"; 3909 return $code . "\n"; 3910} 3911 3912/** @param FuncInfo[] $generatedFuncInfos */ 3913function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo { 3914 foreach ($generatedFuncInfos as $generatedFuncInfo) { 3915 if ($generatedFuncInfo->equalsApartFromNameAndRefcount($funcInfo)) { 3916 return $generatedFuncInfo; 3917 } 3918 } 3919 return null; 3920} 3921 3922/** 3923 * @template T 3924 * @param iterable<T> $infos 3925 * @param Closure(T): string|null $codeGenerator 3926 * @param ?string $parentCond 3927 */ 3928function generateCodeWithConditions( 3929 iterable $infos, string $separator, Closure $codeGenerator, ?string $parentCond = null): string { 3930 $code = ""; 3931 foreach ($infos as $info) { 3932 $infoCode = $codeGenerator($info); 3933 if ($infoCode === null) { 3934 continue; 3935 } 3936 3937 $code .= $separator; 3938 if ($info->cond && $info->cond !== $parentCond) { 3939 $code .= "#if {$info->cond}\n"; 3940 $code .= $infoCode; 3941 $code .= "#endif\n"; 3942 } else { 3943 $code .= $infoCode; 3944 } 3945 } 3946 3947 return $code; 3948} 3949 3950/** 3951 * @param iterable<ConstInfo> $allConstInfos 3952 */ 3953function generateArgInfoCode( 3954 string $stubFilenameWithoutExtension, 3955 FileInfo $fileInfo, 3956 iterable $allConstInfos, 3957 string $stubHash 3958): string { 3959 $code = "/* This is a generated file, edit the .stub.php file instead.\n" 3960 . " * Stub hash: $stubHash */\n"; 3961 3962 $generatedFuncInfos = []; 3963 $code .= generateCodeWithConditions( 3964 $fileInfo->getAllFuncInfos(), "\n", 3965 static function (FuncInfo $funcInfo) use (&$generatedFuncInfos, $fileInfo) { 3966 /* If there already is an equivalent arginfo structure, only emit a #define */ 3967 if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) { 3968 $code = sprintf( 3969 "#define %s %s\n", 3970 $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() 3971 ); 3972 } else { 3973 $code = funcInfoToCode($fileInfo, $funcInfo); 3974 } 3975 3976 $generatedFuncInfos[] = $funcInfo; 3977 return $code; 3978 } 3979 ); 3980 3981 if ($fileInfo->generateFunctionEntries) { 3982 $code .= "\n\n"; 3983 3984 $generatedFunctionDeclarations = []; 3985 $code .= generateCodeWithConditions( 3986 $fileInfo->getAllFuncInfos(), "", 3987 static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarations) { 3988 $key = $funcInfo->getDeclarationKey(); 3989 if (isset($generatedFunctionDeclarations[$key])) { 3990 return null; 3991 } 3992 3993 $generatedFunctionDeclarations[$key] = true; 3994 return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); 3995 } 3996 ); 3997 3998 if (!empty($fileInfo->funcInfos)) { 3999 $code .= generateFunctionEntries(null, $fileInfo->funcInfos); 4000 } 4001 4002 foreach ($fileInfo->classInfos as $classInfo) { 4003 $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos, $classInfo->cond); 4004 } 4005 } 4006 4007 $php82MinimumCompatibility = $fileInfo->generateLegacyArginfoForPhpVersionId === null || $fileInfo->generateLegacyArginfoForPhpVersionId >= PHP_82_VERSION_ID; 4008 4009 if ($fileInfo->generateClassEntries) { 4010 if ($attributeInitializationCode = generateAttributeInitialization($fileInfo->funcInfos, $allConstInfos, null)) { 4011 if (!$php82MinimumCompatibility) { 4012 $attributeInitializationCode = "\n#if (PHP_VERSION_ID >= " . PHP_82_VERSION_ID . ")" . $attributeInitializationCode . "#endif\n"; 4013 } 4014 } 4015 4016 if ($attributeInitializationCode !== "" || !empty($fileInfo->constInfos)) { 4017 $code .= "\nstatic void register_{$stubFilenameWithoutExtension}_symbols(int module_number)\n"; 4018 $code .= "{\n"; 4019 4020 foreach ($fileInfo->constInfos as $constInfo) { 4021 $code .= $constInfo->getDeclaration($allConstInfos); 4022 } 4023 4024 if (!empty($attributeInitializationCode !== "" && $fileInfo->constInfos)) { 4025 $code .= "\n"; 4026 } 4027 4028 $code .= $attributeInitializationCode; 4029 $code .= "}\n"; 4030 } 4031 4032 $code .= generateClassEntryCode($fileInfo, $allConstInfos); 4033 } 4034 4035 return $code; 4036} 4037 4038/** 4039 * @param iterable<ConstInfo> $allConstInfos 4040 */ 4041function generateClassEntryCode(FileInfo $fileInfo, iterable $allConstInfos): string { 4042 $code = ""; 4043 4044 foreach ($fileInfo->classInfos as $class) { 4045 $code .= "\n" . $class->getRegistration($allConstInfos); 4046 } 4047 4048 return $code; 4049} 4050 4051/** @param FuncInfo[] $funcInfos */ 4052function generateFunctionEntries(?Name $className, array $funcInfos, ?string $cond = null): string { 4053 $code = "\n\n"; 4054 4055 if ($cond) { 4056 $code .= "#if {$cond}\n"; 4057 } 4058 4059 $functionEntryName = "ext_functions"; 4060 if ($className) { 4061 $underscoreName = implode("_", $className->parts); 4062 $functionEntryName = "class_{$underscoreName}_methods"; 4063 } 4064 4065 $code .= "static const zend_function_entry {$functionEntryName}[] = {\n"; 4066 $code .= generateCodeWithConditions($funcInfos, "", static function (FuncInfo $funcInfo) { 4067 return $funcInfo->getFunctionEntry(); 4068 }, $cond); 4069 $code .= "\tZEND_FE_END\n"; 4070 $code .= "};\n"; 4071 4072 if ($cond) { 4073 $code .= "#endif\n"; 4074 } 4075 4076 return $code; 4077} 4078/** 4079 * @param iterable<FuncInfo> $funcInfos 4080 */ 4081function generateAttributeInitialization(iterable $funcInfos, iterable $allConstInfos, ?string $parentCond = null): string { 4082 return generateCodeWithConditions( 4083 $funcInfos, 4084 "", 4085 static function (FuncInfo $funcInfo) use ($allConstInfos) { 4086 $code = null; 4087 4088 foreach ($funcInfo->args as $index => $arg) { 4089 if ($funcInfo->name instanceof MethodName) { 4090 $functionTable = "&class_entry->function_table"; 4091 } else { 4092 $functionTable = "CG(function_table)"; 4093 } 4094 4095 foreach ($arg->attributes as $attribute) { 4096 $code .= $attribute->generateCode("zend_add_parameter_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1), $index", "{$funcInfo->name->getMethodSynopsisFilename()}_arg{$index}", $allConstInfos); 4097 } 4098 } 4099 4100 return $code; 4101 }, 4102 $parentCond 4103 ); 4104} 4105 4106/** @param array<string, FuncInfo> $funcMap */ 4107function generateOptimizerInfo(array $funcMap): string { 4108 4109 $code = "/* This is a generated file, edit the .stub.php files instead. */\n\n"; 4110 4111 $code .= "static const func_info_t func_infos[] = {\n"; 4112 4113 $code .= generateCodeWithConditions($funcMap, "", static function (FuncInfo $funcInfo) { 4114 return $funcInfo->getOptimizerInfo(); 4115 }); 4116 4117 $code .= "};\n"; 4118 4119 return $code; 4120} 4121 4122/** 4123 * @param array<int, string[]> $flagsByPhpVersions 4124 * @return string[] 4125 */ 4126function generateVersionDependentFlagCode(string $codeTemplate, array $flagsByPhpVersions, ?int $phpVersionIdMinimumCompatibility): array 4127{ 4128 $phpVersions = ALL_PHP_VERSION_IDS; 4129 sort($phpVersions); 4130 $currentPhpVersion = end($phpVersions); 4131 4132 // No version compatibility is needed 4133 if ($phpVersionIdMinimumCompatibility === null) { 4134 if (empty($flagsByPhpVersions[$currentPhpVersion])) { 4135 return []; 4136 } 4137 4138 return [sprintf($codeTemplate, implode("|", $flagsByPhpVersions[$currentPhpVersion]))]; 4139 } 4140 4141 // Remove flags which depend on a PHP version below the minimally supported one 4142 ksort($flagsByPhpVersions); 4143 $index = array_search($phpVersionIdMinimumCompatibility, array_keys($flagsByPhpVersions)); 4144 if ($index === false) { 4145 throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMinimumCompatibility\""); 4146 } 4147 $flagsByPhpVersions = array_slice($flagsByPhpVersions, $index, null, true); 4148 4149 // Remove empty version-specific flags 4150 $flagsByPhpVersions = array_filter( 4151 $flagsByPhpVersions, 4152 static function (array $value): bool { 4153 return !empty($value); 4154 }); 4155 4156 // There are no version-specific flags 4157 if (empty($flagsByPhpVersions)) { 4158 return []; 4159 } 4160 4161 // Remove version-specific flags which don't differ from the previous one 4162 $previousVersionId = null; 4163 foreach ($flagsByPhpVersions as $versionId => $versionFlags) { 4164 if ($previousVersionId !== null && $flagsByPhpVersions[$previousVersionId] === $versionFlags) { 4165 unset($flagsByPhpVersions[$versionId]); 4166 } else { 4167 $previousVersionId = $versionId; 4168 } 4169 } 4170 4171 $flagCount = count($flagsByPhpVersions); 4172 4173 // Do not add a condition unnecessarily when the only version is the same as the minimally supported one 4174 if ($flagCount === 1) { 4175 reset($flagsByPhpVersions); 4176 $firstVersion = key($flagsByPhpVersions); 4177 if ($firstVersion === $phpVersionIdMinimumCompatibility) { 4178 return [sprintf($codeTemplate, implode("|", reset($flagsByPhpVersions)))]; 4179 } 4180 } 4181 4182 // Add the necessary conditions around the code using the version-specific flags 4183 $result = []; 4184 $i = 0; 4185 foreach (array_reverse($flagsByPhpVersions, true) as $version => $versionFlags) { 4186 $code = ""; 4187 4188 $if = $i === 0 ? "#if" : "#elif"; 4189 $endif = $i === $flagCount - 1 ? "#endif\n" : ""; 4190 4191 $code .= "$if (PHP_VERSION_ID >= $version)\n"; 4192 4193 $code .= sprintf($codeTemplate, implode("|", $versionFlags)); 4194 $code .= $endif; 4195 4196 $result[] = $code; 4197 $i++; 4198 } 4199 4200 return $result; 4201} 4202 4203/** 4204 * @param array<string, ClassInfo> $classMap 4205 * @param iterable<ConstInfo> $allConstInfos 4206 * @return array<string, string> 4207 */ 4208function generateClassSynopses(array $classMap, iterable $allConstInfos): array { 4209 $result = []; 4210 4211 foreach ($classMap as $classInfo) { 4212 $classSynopsis = $classInfo->getClassSynopsisDocument($classMap, $allConstInfos); 4213 if ($classSynopsis !== null) { 4214 $result[ClassInfo::getClassSynopsisFilename($classInfo->name) . ".xml"] = $classSynopsis; 4215 } 4216 } 4217 4218 return $result; 4219} 4220 4221/** 4222 * @param array<string, ClassInfo> $classMap 4223 * $param iterable<ConstInfo> $allConstInfos 4224 * @return array<string, string> 4225 */ 4226function replaceClassSynopses(string $targetDirectory, array $classMap, iterable $allConstInfos, bool $isVerify): array 4227{ 4228 $existingClassSynopses = []; 4229 4230 $classSynopses = []; 4231 4232 $it = new RecursiveIteratorIterator( 4233 new RecursiveDirectoryIterator($targetDirectory), 4234 RecursiveIteratorIterator::LEAVES_ONLY 4235 ); 4236 4237 foreach ($it as $file) { 4238 $pathName = $file->getPathName(); 4239 if (!preg_match('/\.xml$/i', $pathName)) { 4240 continue; 4241 } 4242 4243 $xml = file_get_contents($pathName); 4244 if ($xml === false) { 4245 continue; 4246 } 4247 4248 if (stripos($xml, "<classsynopsis") === false) { 4249 continue; 4250 } 4251 4252 $replacedXml = getReplacedSynopsisXml($xml); 4253 4254 $doc = new DOMDocument(); 4255 $doc->formatOutput = false; 4256 $doc->preserveWhiteSpace = true; 4257 $doc->validateOnParse = true; 4258 $success = $doc->loadXML($replacedXml); 4259 if (!$success) { 4260 echo "Failed opening $pathName\n"; 4261 continue; 4262 } 4263 4264 $classSynopsisElements = []; 4265 foreach ($doc->getElementsByTagName("classsynopsis") as $element) { 4266 $classSynopsisElements[] = $element; 4267 } 4268 4269 foreach ($classSynopsisElements as $classSynopsis) { 4270 if (!$classSynopsis instanceof DOMElement) { 4271 continue; 4272 } 4273 4274 $firstChild = $classSynopsis->firstElementChild; 4275 if ($firstChild === null) { 4276 continue; 4277 } 4278 $firstChild = $firstChild->firstElementChild; 4279 if ($firstChild === null) { 4280 continue; 4281 } 4282 $className = $firstChild->textContent; 4283 if (!isset($classMap[$className])) { 4284 continue; 4285 } 4286 4287 $existingClassSynopses[$className] = $className; 4288 4289 $classInfo = $classMap[$className]; 4290 4291 $newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap, $allConstInfos); 4292 if ($newClassSynopsis === null) { 4293 continue; 4294 } 4295 4296 // Check if there is any change - short circuit if there is not any. 4297 4298 if (replaceAndCompareXmls($doc, $classSynopsis, $newClassSynopsis)) { 4299 continue; 4300 } 4301 4302 // Return the updated XML 4303 4304 $replacedXml = $doc->saveXML(); 4305 4306 $replacedXml = preg_replace( 4307 [ 4308 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 4309 '/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([^"]+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4310 '/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([^"]+)"\s+xmlns="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4311 '/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([^"]+)"\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4312 '/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4313 '/<phpdoc:(classref|exceptionref)\s+xmlns=\"([^"]+)\"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xmlns:phpdoc="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4314 ], 4315 [ 4316 "&$1", 4317 "<phpdoc:$1 xml:id=\"$4\" xmlns:phpdoc=\"$2\" xmlns=\"$3\">", 4318 "<phpdoc:$1 xml:id=\"$5\" xmlns:phpdoc=\"$2\" xmlns=\"$3\" xmlns:xi=\"$4\">", 4319 "<phpdoc:$1 xml:id=\"$6\" xmlns:phpdoc=\"$2\" xmlns=\"$3\" xmlns:xlink=\"$4\" xmlns:xi=\"$5\">", 4320 "<phpdoc:$1 xml:id=\"$6\" xmlns:phpdoc=\"$2\" xmlns=\"$5\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">", 4321 "<phpdoc:$1 xml:id=\"$6\" xmlns:phpdoc=\"$5\" xmlns=\"$2\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">", 4322 ], 4323 $replacedXml 4324 ); 4325 4326 $classSynopses[$pathName] = $replacedXml; 4327 } 4328 } 4329 4330 if ($isVerify) { 4331 $missingClassSynopses = array_diff_key($classMap, $existingClassSynopses); 4332 foreach ($missingClassSynopses as $className => $info) { 4333 /** @var ClassInfo $info */ 4334 if (!$info->isUndocumentable) { 4335 echo "Warning: Missing class synopsis for $className\n"; 4336 } 4337 } 4338 } 4339 4340 return $classSynopses; 4341} 4342 4343function getReplacedSynopsisXml(string $xml): string 4344{ 4345 return preg_replace( 4346 [ 4347 "/&([A-Za-z0-9._{}%-]+?;)/", 4348 "/<(\/)*xi:([A-Za-z]+?)/" 4349 ], 4350 [ 4351 "REPLACED-ENTITY-$1", 4352 "<$1XI$2", 4353 ], 4354 $xml 4355 ); 4356} 4357 4358/** 4359 * @param array<string, FuncInfo> $funcMap 4360 * @param array<string, FuncInfo> $aliasMap 4361 * @return array<string, string> 4362 */ 4363function generateMethodSynopses(array $funcMap, array $aliasMap): array { 4364 $result = []; 4365 4366 foreach ($funcMap as $funcInfo) { 4367 $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap); 4368 if ($methodSynopsis !== null) { 4369 $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis; 4370 } 4371 } 4372 4373 return $result; 4374} 4375 4376/** 4377 * @param array<string, FuncInfo> $funcMap 4378 * @param array<string, FuncInfo> $aliasMap 4379 * @return array<string, string> 4380 */ 4381function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $aliasMap, bool $isVerify): array { 4382 $existingMethodSynopses = []; 4383 $methodSynopses = []; 4384 4385 $it = new RecursiveIteratorIterator( 4386 new RecursiveDirectoryIterator($targetDirectory), 4387 RecursiveIteratorIterator::LEAVES_ONLY 4388 ); 4389 4390 foreach ($it as $file) { 4391 $pathName = $file->getPathName(); 4392 if (!preg_match('/\.xml$/i', $pathName)) { 4393 continue; 4394 } 4395 4396 $xml = file_get_contents($pathName); 4397 if ($xml === false) { 4398 continue; 4399 } 4400 4401 if ($isVerify) { 4402 $matches = []; 4403 preg_match("/<refname>\s*([\w:]+)\s*<\/refname>\s*<refpurpose>\s*&Alias;\s*<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>\s*<\/refpurpose>/i", $xml, $matches); 4404 $aliasName = $matches[1] ?? null; 4405 $alias = $funcMap[$aliasName] ?? null; 4406 $funcName = $matches[2] ?? null; 4407 $func = $funcMap[$funcName] ?? null; 4408 4409 if ($alias && 4410 !$alias->isUndocumentable && 4411 ($func === null || $func->alias === null || $func->alias->__toString() !== $aliasName) && 4412 ($alias->alias === null || $alias->alias->__toString() !== $funcName) 4413 ) { 4414 echo "Warning: $aliasName()" . ($alias->alias ? " is an alias of " . $alias->alias->__toString() . "(), but it" : "") . " is incorrectly documented as an alias for $funcName()\n"; 4415 } 4416 4417 $matches = []; 4418 preg_match("/<(?:para|simpara)>\s*(?:&info.function.alias;|&info.method.alias;|&Alias;)\s+<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>/i", $xml, $matches); 4419 $descriptionFuncName = $matches[1] ?? null; 4420 $descriptionFunc = $funcMap[$descriptionFuncName] ?? null; 4421 if ($descriptionFunc && $funcName !== $descriptionFuncName) { 4422 echo "Warning: Alias in the method synopsis description of $pathName doesn't match the alias in the <refpurpose>\n"; 4423 } 4424 4425 if ($aliasName) { 4426 $existingMethodSynopses[$aliasName] = $aliasName; 4427 } 4428 } 4429 4430 if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) { 4431 continue; 4432 } 4433 4434 $replacedXml = getReplacedSynopsisXml($xml); 4435 4436 $doc = new DOMDocument(); 4437 $doc->formatOutput = false; 4438 $doc->preserveWhiteSpace = true; 4439 $doc->validateOnParse = true; 4440 $success = $doc->loadXML($replacedXml); 4441 if (!$success) { 4442 echo "Failed opening $pathName\n"; 4443 continue; 4444 } 4445 4446 $methodSynopsisElements = []; 4447 foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) { 4448 $methodSynopsisElements[] = $element; 4449 } 4450 foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) { 4451 $methodSynopsisElements[] = $element; 4452 } 4453 foreach ($doc->getElementsByTagName("methodsynopsis") as $element) { 4454 $methodSynopsisElements[] = $element; 4455 } 4456 4457 foreach ($methodSynopsisElements as $methodSynopsis) { 4458 if (!$methodSynopsis instanceof DOMElement) { 4459 continue; 4460 } 4461 4462 $list = $methodSynopsis->getElementsByTagName("methodname"); 4463 $item = $list->item(0); 4464 if (!$item instanceof DOMElement) { 4465 continue; 4466 } 4467 $funcName = $item->textContent; 4468 if (!isset($funcMap[$funcName])) { 4469 continue; 4470 } 4471 4472 $funcInfo = $funcMap[$funcName]; 4473 $existingMethodSynopses[$funcInfo->name->__toString()] = $funcInfo->name->__toString(); 4474 4475 $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 4476 if ($newMethodSynopsis === null) { 4477 continue; 4478 } 4479 4480 // Retrieve current signature 4481 4482 $params = []; 4483 $list = $methodSynopsis->getElementsByTagName("methodparam"); 4484 foreach ($list as $i => $item) { 4485 if (!$item instanceof DOMElement) { 4486 continue; 4487 } 4488 4489 $paramList = $item->getElementsByTagName("parameter"); 4490 if ($paramList->count() !== 1) { 4491 continue; 4492 } 4493 4494 $paramName = $paramList->item(0)->textContent; 4495 $paramTypes = []; 4496 4497 $paramList = $item->getElementsByTagName("type"); 4498 foreach ($paramList as $type) { 4499 if (!$type instanceof DOMElement) { 4500 continue; 4501 } 4502 4503 $paramTypes[] = $type->textContent; 4504 } 4505 4506 $params[$paramName] = ["index" => $i, "type" => $paramTypes]; 4507 } 4508 4509 // Check if there is any change - short circuit if there is not any. 4510 4511 if (replaceAndCompareXmls($doc, $methodSynopsis, $newMethodSynopsis)) { 4512 continue; 4513 } 4514 4515 // Update parameter references 4516 4517 $paramList = $doc->getElementsByTagName("parameter"); 4518 /** @var DOMElement $paramElement */ 4519 foreach ($paramList as $paramElement) { 4520 if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") { 4521 continue; 4522 } 4523 4524 $name = $paramElement->textContent; 4525 if (!isset($params[$name])) { 4526 continue; 4527 } 4528 4529 $index = $params[$name]["index"]; 4530 if (!isset($funcInfo->args[$index])) { 4531 continue; 4532 } 4533 4534 $paramElement->textContent = $funcInfo->args[$index]->name; 4535 } 4536 4537 // Return the updated XML 4538 4539 $replacedXml = $doc->saveXML(); 4540 4541 $replacedXml = preg_replace( 4542 [ 4543 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 4544 '/<refentry\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4545 '/<refentry\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 4546 ], 4547 [ 4548 "&$1", 4549 "<refentry xml:id=\"$2\" xmlns=\"$1\">", 4550 "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 4551 ], 4552 $replacedXml 4553 ); 4554 4555 $methodSynopses[$pathName] = $replacedXml; 4556 } 4557 } 4558 4559 if ($isVerify) { 4560 $missingMethodSynopses = array_diff_key($funcMap, $existingMethodSynopses); 4561 foreach ($missingMethodSynopses as $functionName => $info) { 4562 /** @var FuncInfo $info */ 4563 if (!$info->isUndocumentable) { 4564 echo "Warning: Missing method synopsis for $functionName()\n"; 4565 } 4566 } 4567 } 4568 4569 return $methodSynopses; 4570} 4571 4572function replaceAndCompareXmls(DOMDocument $doc, DOMElement $originalSynopsis, DOMElement $newSynopsis): bool 4573{ 4574 $docComparator = new DOMDocument(); 4575 $docComparator->preserveWhiteSpace = false; 4576 $docComparator->formatOutput = true; 4577 4578 $xml1 = $doc->saveXML($originalSynopsis); 4579 $xml1 = getReplacedSynopsisXml($xml1); 4580 $docComparator->loadXML($xml1); 4581 $xml1 = $docComparator->saveXML(); 4582 4583 $originalSynopsis->parentNode->replaceChild($newSynopsis, $originalSynopsis); 4584 4585 $xml2 = $doc->saveXML($newSynopsis); 4586 $xml2 = getReplacedSynopsisXml($xml2); 4587 4588 $docComparator->loadXML($xml2); 4589 $xml2 = $docComparator->saveXML(); 4590 4591 return $xml1 === $xml2; 4592} 4593 4594function installPhpParser(string $version, string $phpParserDir) { 4595 $lockFile = __DIR__ . "/PHP-Parser-install-lock"; 4596 $lockFd = fopen($lockFile, 'w+'); 4597 if (!flock($lockFd, LOCK_EX)) { 4598 throw new Exception("Failed to acquire installation lock"); 4599 } 4600 4601 try { 4602 // Check whether a parallel process has already installed PHP-Parser. 4603 if (is_dir($phpParserDir)) { 4604 return; 4605 } 4606 4607 $cwd = getcwd(); 4608 chdir(__DIR__); 4609 4610 $tarName = "v$version.tar.gz"; 4611 passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 4612 if ($exit !== 0) { 4613 passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 4614 } 4615 if ($exit !== 0) { 4616 throw new Exception("Failed to download PHP-Parser tarball"); 4617 } 4618 if (!mkdir($phpParserDir)) { 4619 throw new Exception("Failed to create directory $phpParserDir"); 4620 } 4621 passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit); 4622 if ($exit !== 0) { 4623 throw new Exception("Failed to extract PHP-Parser tarball"); 4624 } 4625 unlink(__DIR__ . "/$tarName"); 4626 chdir($cwd); 4627 } finally { 4628 flock($lockFd, LOCK_UN); 4629 @unlink($lockFile); 4630 } 4631} 4632 4633function initPhpParser() { 4634 static $isInitialized = false; 4635 if ($isInitialized) { 4636 return; 4637 } 4638 4639 if (!extension_loaded("tokenizer")) { 4640 throw new Exception("The \"tokenizer\" extension is not available"); 4641 } 4642 4643 $isInitialized = true; 4644 $version = "4.15.1"; 4645 $phpParserDir = __DIR__ . "/PHP-Parser-$version"; 4646 if (!is_dir($phpParserDir)) { 4647 installPhpParser($version, $phpParserDir); 4648 } 4649 4650 spl_autoload_register(static function(string $class) use ($phpParserDir) { 4651 if (strpos($class, "PhpParser\\") === 0) { 4652 $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php"; 4653 require $fileName; 4654 } 4655 }); 4656} 4657 4658$optind = null; 4659$options = getopt( 4660 "fh", 4661 [ 4662 "force-regeneration", "parameter-stats", "help", "verify", "generate-classsynopses", "replace-classsynopses", 4663 "generate-methodsynopses", "replace-methodsynopses", "generate-optimizer-info" 4664 ], 4665 $optind 4666); 4667 4668$context = new Context; 4669$printParameterStats = isset($options["parameter-stats"]); 4670$verify = isset($options["verify"]); 4671$generateClassSynopses = isset($options["generate-classsynopses"]); 4672$replaceClassSynopses = isset($options["replace-classsynopses"]); 4673$generateMethodSynopses = isset($options["generate-methodsynopses"]); 4674$replaceMethodSynopses = isset($options["replace-methodsynopses"]); 4675$generateOptimizerInfo = isset($options["generate-optimizer-info"]); 4676$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); 4677$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateClassSynopses || $generateOptimizerInfo || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses; 4678 4679$targetSynopses = $argv[$argc - 1] ?? null; 4680if ($replaceClassSynopses && $targetSynopses === null) { 4681 die("A target class synopsis directory must be provided for.\n"); 4682} 4683 4684if ($replaceMethodSynopses && $targetSynopses === null) { 4685 die("A target method synopsis directory must be provided.\n"); 4686} 4687 4688if (isset($options["h"]) || isset($options["help"])) { 4689 die("\nusage: gen_stub.php [ -f | --force-regeneration ] [ --generate-classsynopses ] [ --replace-classsynopses ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ --generate-optimizer-info ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n"); 4690} 4691 4692$fileInfos = []; 4693$locations = array_slice($argv, $optind) ?: ['.']; 4694foreach (array_unique($locations) as $location) { 4695 if (is_file($location)) { 4696 // Generate single file. 4697 $fileInfo = processStubFile($location, $context); 4698 if ($fileInfo) { 4699 $fileInfos[] = $fileInfo; 4700 } 4701 } else if (is_dir($location)) { 4702 array_push($fileInfos, ...processDirectory($location, $context)); 4703 } else { 4704 echo "$location is neither a file nor a directory.\n"; 4705 exit(1); 4706 } 4707} 4708 4709if ($printParameterStats) { 4710 $parameterStats = []; 4711 4712 foreach ($fileInfos as $fileInfo) { 4713 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 4714 foreach ($funcInfo->args as $argInfo) { 4715 if (!isset($parameterStats[$argInfo->name])) { 4716 $parameterStats[$argInfo->name] = 0; 4717 } 4718 $parameterStats[$argInfo->name]++; 4719 } 4720 } 4721 } 4722 4723 arsort($parameterStats); 4724 echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; 4725} 4726 4727/** @var array<string, ClassInfo> $classMap */ 4728$classMap = []; 4729/** @var array<string, FuncInfo> $funcMap */ 4730$funcMap = []; 4731/** @var array<string, FuncInfo> $aliasMap */ 4732$aliasMap = []; 4733 4734foreach ($fileInfos as $fileInfo) { 4735 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 4736 $funcMap[$funcInfo->name->__toString()] = $funcInfo; 4737 4738 // TODO: Don't use aliasMap for methodsynopsis? 4739 if ($funcInfo->aliasType === "alias") { 4740 $aliasMap[$funcInfo->alias->__toString()] = $funcInfo; 4741 } 4742 } 4743 4744 foreach ($fileInfo->classInfos as $classInfo) { 4745 $classMap[$classInfo->name->__toString()] = $classInfo; 4746 } 4747} 4748 4749if ($verify) { 4750 $errors = []; 4751 4752 foreach ($funcMap as $aliasFunc) { 4753 if (!$aliasFunc->alias) { 4754 continue; 4755 } 4756 4757 if (!isset($funcMap[$aliasFunc->alias->__toString()])) { 4758 $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found"; 4759 continue; 4760 } 4761 4762 if (!$aliasFunc->verify) { 4763 continue; 4764 } 4765 4766 $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()]; 4767 $aliasedArgs = $aliasedFunc->args; 4768 $aliasArgs = $aliasFunc->args; 4769 4770 if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) { 4771 if ($aliasFunc->isInstanceMethod()) { 4772 $aliasedArgs = array_slice($aliasedArgs, 1); 4773 } 4774 4775 if ($aliasedFunc->isInstanceMethod()) { 4776 $aliasArgs = array_slice($aliasArgs, 1); 4777 } 4778 } 4779 4780 array_map( 4781 function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { 4782 if ($aliasArg === null) { 4783 assert($aliasedArg !== null); 4784 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; 4785 return null; 4786 } 4787 4788 if ($aliasedArg === null) { 4789 $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; 4790 return null; 4791 } 4792 4793 if ($aliasArg->name !== $aliasedArg->name) { 4794 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; 4795 return null; 4796 } 4797 4798 if ($aliasArg->type != $aliasedArg->type) { 4799 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; 4800 } 4801 4802 if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { 4803 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; 4804 } 4805 }, 4806 $aliasArgs, $aliasedArgs 4807 ); 4808 4809 $aliasedReturn = $aliasedFunc->return; 4810 $aliasReturn = $aliasFunc->return; 4811 4812 if (!$aliasedFunc->name->isConstructor() && !$aliasFunc->name->isConstructor()) { 4813 $aliasedReturnType = $aliasedReturn->type ?? $aliasedReturn->phpDocType; 4814 $aliasReturnType = $aliasReturn->type ?? $aliasReturn->phpDocType; 4815 if ($aliasReturnType != $aliasedReturnType) { 4816 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type"; 4817 } 4818 } 4819 4820 $aliasedPhpDocReturnType = $aliasedReturn->phpDocType; 4821 $aliasPhpDocReturnType = $aliasReturn->phpDocType; 4822 if ($aliasedPhpDocReturnType != $aliasPhpDocReturnType && $aliasedPhpDocReturnType != $aliasReturn->type && $aliasPhpDocReturnType != $aliasedReturn->type) { 4823 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same PHPDoc return type"; 4824 } 4825 } 4826 4827 echo implode("\n", $errors); 4828 if (!empty($errors)) { 4829 echo "\n"; 4830 exit(1); 4831 } 4832} 4833 4834if ($generateClassSynopses) { 4835 $classSynopsesDirectory = getcwd() . "/classsynopses"; 4836 4837 $classSynopses = generateClassSynopses($classMap, $context->allConstInfos); 4838 if (!empty($classSynopses)) { 4839 if (!file_exists($classSynopsesDirectory)) { 4840 mkdir($classSynopsesDirectory); 4841 } 4842 4843 foreach ($classSynopses as $filename => $content) { 4844 if (file_put_contents("$classSynopsesDirectory/$filename", $content)) { 4845 echo "Saved $filename\n"; 4846 } 4847 } 4848 } 4849} 4850 4851if ($replaceClassSynopses) { 4852 $classSynopses = replaceClassSynopses($targetSynopses, $classMap, $context->allConstInfos, $verify); 4853 4854 foreach ($classSynopses as $filename => $content) { 4855 if (file_put_contents($filename, $content)) { 4856 echo "Saved $filename\n"; 4857 } 4858 } 4859} 4860 4861if ($generateMethodSynopses) { 4862 $methodSynopsesDirectory = getcwd() . "/methodsynopses"; 4863 4864 $methodSynopses = generateMethodSynopses($funcMap, $aliasMap); 4865 if (!empty($methodSynopses)) { 4866 if (!file_exists($methodSynopsesDirectory)) { 4867 mkdir($methodSynopsesDirectory); 4868 } 4869 4870 foreach ($methodSynopses as $filename => $content) { 4871 if (file_put_contents("$methodSynopsesDirectory/$filename", $content)) { 4872 echo "Saved $filename\n"; 4873 } 4874 } 4875 } 4876} 4877 4878if ($replaceMethodSynopses) { 4879 $methodSynopses = replaceMethodSynopses($targetSynopses, $funcMap, $aliasMap, $verify); 4880 4881 foreach ($methodSynopses as $filename => $content) { 4882 if (file_put_contents($filename, $content)) { 4883 echo "Saved $filename\n"; 4884 } 4885 } 4886} 4887 4888if ($generateOptimizerInfo) { 4889 $filename = dirname(__FILE__, 2) . "/Zend/Optimizer/zend_func_infos.h"; 4890 $optimizerInfo = generateOptimizerInfo($funcMap); 4891 4892 if (file_put_contents($filename, $optimizerInfo)) { 4893 echo "Saved $filename\n"; 4894 } 4895} 4896