1#!/usr/bin/env php 2<?php declare(strict_types=1); 3 4use PhpParser\Comment\Doc as DocComment; 5use PhpParser\ConstExprEvaluator; 6use PhpParser\Modifiers; 7use PhpParser\Node; 8use PhpParser\Node\AttributeGroup; 9use PhpParser\Node\Expr; 10use PhpParser\Node\Name; 11use PhpParser\Node\Stmt; 12use PhpParser\Node\Stmt\Class_; 13use PhpParser\Node\Stmt\Enum_; 14use PhpParser\Node\Stmt\Interface_; 15use PhpParser\Node\Stmt\Trait_; 16use PhpParser\PrettyPrinter\Standard; 17use PhpParser\PrettyPrinterAbstract; 18 19error_reporting(E_ALL); 20ini_set("precision", "-1"); 21 22const PHP_70_VERSION_ID = 70000; 23const PHP_80_VERSION_ID = 80000; 24const PHP_81_VERSION_ID = 80100; 25const PHP_82_VERSION_ID = 80200; 26const PHP_83_VERSION_ID = 80300; 27const PHP_84_VERSION_ID = 80400; 28const ALL_PHP_VERSION_IDS = [ 29 PHP_70_VERSION_ID, 30 PHP_80_VERSION_ID, 31 PHP_81_VERSION_ID, 32 PHP_82_VERSION_ID, 33 PHP_83_VERSION_ID, 34 PHP_84_VERSION_ID, 35]; 36 37/** 38 * @return FileInfo[] 39 */ 40function processDirectory(string $dir, Context $context): array { 41 $pathNames = []; 42 $it = new RecursiveIteratorIterator( 43 new RecursiveDirectoryIterator($dir), 44 RecursiveIteratorIterator::LEAVES_ONLY 45 ); 46 foreach ($it as $file) { 47 $pathName = $file->getPathName(); 48 if (preg_match('/\.stub\.php$/', $pathName)) { 49 $pathNames[] = $pathName; 50 } 51 } 52 53 // Make sure stub files are processed in a predictable, system-independent order. 54 sort($pathNames); 55 56 $fileInfos = []; 57 foreach ($pathNames as $pathName) { 58 $fileInfo = processStubFile($pathName, $context); 59 if ($fileInfo) { 60 $fileInfos[] = $fileInfo; 61 } 62 } 63 return $fileInfos; 64} 65 66function processStubFile(string $stubFile, Context $context, bool $includeOnly = false): ?FileInfo { 67 try { 68 if (!file_exists($stubFile)) { 69 throw new Exception("File $stubFile does not exist"); 70 } 71 72 if (!$includeOnly) { 73 $stubFilenameWithoutExtension = str_replace(".stub.php", "", $stubFile); 74 $arginfoFile = "{$stubFilenameWithoutExtension}_arginfo.h"; 75 $legacyFile = "{$stubFilenameWithoutExtension}_legacy_arginfo.h"; 76 77 $stubCode = file_get_contents($stubFile); 78 $stubHash = computeStubHash($stubCode); 79 $oldStubHash = extractStubHash($arginfoFile); 80 if ($stubHash === $oldStubHash && !$context->forceParse) { 81 /* Stub file did not change, do not regenerate. */ 82 return null; 83 } 84 } 85 86 /* Because exit() and die() are proper token/keywords we need to hack-around */ 87 $hasSpecialExitAsFunctionHandling = str_ends_with($stubFile, 'zend_builtin_functions.stub.php'); 88 if (!$fileInfo = $context->parsedFiles[$stubFile] ?? null) { 89 initPhpParser(); 90 $stubContent = $stubCode ?? file_get_contents($stubFile); 91 if ($hasSpecialExitAsFunctionHandling) { 92 $stubContent = str_replace(['exit', 'die'], ['exit_dummy', 'die_dummy'], $stubContent); 93 } 94 $fileInfo = parseStubFile($stubContent); 95 $context->parsedFiles[$stubFile] = $fileInfo; 96 97 foreach ($fileInfo->dependencies as $dependency) { 98 // TODO add header search path for extensions? 99 $prefixes = [dirname($stubFile) . "/", dirname(__DIR__) . "/"]; 100 foreach ($prefixes as $prefix) { 101 $depFile = $prefix . $dependency; 102 if (file_exists($depFile)) { 103 break; 104 } 105 $depFile = null; 106 } 107 if (!$depFile) { 108 throw new Exception("File $stubFile includes a file $dependency which does not exist"); 109 } 110 processStubFile($depFile, $context, true); 111 } 112 113 $constInfos = $fileInfo->getAllConstInfos(); 114 $context->allConstInfos = array_merge($context->allConstInfos, $constInfos); 115 } 116 117 if ($includeOnly) { 118 return $fileInfo; 119 } 120 121 $arginfoCode = generateArgInfoCode( 122 basename($stubFilenameWithoutExtension), 123 $fileInfo, 124 $context->allConstInfos, 125 $stubHash 126 ); 127 if ($hasSpecialExitAsFunctionHandling) { 128 $arginfoCode = str_replace(['exit_dummy', 'die_dummy'], ['exit', 'die'], $arginfoCode); 129 } 130 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { 131 echo "Saved $arginfoFile\n"; 132 } 133 134 if ($fileInfo->shouldGenerateLegacyArginfo()) { 135 $legacyFileInfo = clone $fileInfo; 136 $legacyFileInfo->legacyArginfoGeneration = true; 137 $phpVersionIdMinimumCompatibility = $legacyFileInfo->getMinimumPhpVersionIdCompatibility(); 138 139 foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) { 140 $funcInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); 141 } 142 foreach ($legacyFileInfo->getAllClassInfos() as $classInfo) { 143 $classInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); 144 } 145 foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) { 146 $constInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); 147 } 148 149 $arginfoCode = generateArgInfoCode( 150 basename($stubFilenameWithoutExtension), 151 $legacyFileInfo, 152 $context->allConstInfos, 153 $stubHash 154 ); 155 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { 156 echo "Saved $legacyFile\n"; 157 } 158 } 159 160 return $fileInfo; 161 } catch (Exception $e) { 162 echo "In $stubFile:\n{$e->getMessage()}\n"; 163 exit(1); 164 } 165} 166 167function computeStubHash(string $stubCode): string { 168 return sha1(str_replace("\r\n", "\n", $stubCode)); 169} 170 171function extractStubHash(string $arginfoFile): ?string { 172 if (!file_exists($arginfoFile)) { 173 return null; 174 } 175 176 $arginfoCode = file_get_contents($arginfoFile); 177 if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) { 178 return null; 179 } 180 181 return $matches[1]; 182} 183 184class Context { 185 public bool $forceParse = false; 186 public bool $forceRegeneration = false; 187 /** @var array<string, ConstInfo> */ 188 public array $allConstInfos = []; 189 /** @var FileInfo[] */ 190 public array $parsedFiles = []; 191} 192 193class ArrayType extends SimpleType { 194 public Type $keyType; 195 public Type $valueType; 196 197 public static function createGenericArray(): self 198 { 199 return new ArrayType(Type::fromString("int|string"), Type::fromString("mixed|ref")); 200 } 201 202 public function __construct(Type $keyType, Type $valueType) 203 { 204 parent::__construct("array", true); 205 206 $this->keyType = $keyType; 207 $this->valueType = $valueType; 208 } 209 210 public function toOptimizerTypeMask(): string { 211 $typeMasks = [ 212 parent::toOptimizerTypeMask(), 213 $this->keyType->toOptimizerTypeMaskForArrayKey(), 214 $this->valueType->toOptimizerTypeMaskForArrayValue(), 215 ]; 216 217 return implode("|", $typeMasks); 218 } 219 220 public function equals(SimpleType $other): bool { 221 if (!parent::equals($other)) { 222 return false; 223 } 224 225 assert(get_class($other) === self::class); 226 227 return Type::equals($this->keyType, $other->keyType) && 228 Type::equals($this->valueType, $other->valueType); 229 } 230} 231 232class SimpleType { 233 public string $name; 234 public bool $isBuiltin; 235 236 public static function fromNode(Node $node): SimpleType { 237 if ($node instanceof Node\Name) { 238 if ($node->toLowerString() === 'static') { 239 // PHP internally considers "static" a builtin type. 240 return new SimpleType($node->toLowerString(), true); 241 } 242 243 if ($node->toLowerString() === 'self') { 244 throw new Exception('The exact class name must be used instead of "self"'); 245 } 246 247 assert($node->isFullyQualified()); 248 return new SimpleType($node->toString(), false); 249 } 250 251 if ($node instanceof Node\Identifier) { 252 if ($node->toLowerString() === 'array') { 253 return ArrayType::createGenericArray(); 254 } 255 256 return new SimpleType($node->toLowerString(), true); 257 } 258 259 throw new Exception("Unexpected node type"); 260 } 261 262 public static function fromString(string $typeString): SimpleType 263 { 264 switch (strtolower($typeString)) { 265 case "void": 266 case "null": 267 case "false": 268 case "true": 269 case "bool": 270 case "int": 271 case "float": 272 case "string": 273 case "callable": 274 case "object": 275 case "resource": 276 case "mixed": 277 case "static": 278 case "never": 279 case "ref": 280 return new SimpleType(strtolower($typeString), true); 281 case "array": 282 return ArrayType::createGenericArray(); 283 case "self": 284 throw new Exception('The exact class name must be used instead of "self"'); 285 case "iterable": 286 throw new Exception('This should not happen'); 287 } 288 289 $matches = []; 290 $isArray = preg_match("/(.*)\s*\[\s*\]/", $typeString, $matches); 291 if ($isArray) { 292 return new ArrayType(Type::fromString("int"), Type::fromString($matches[1])); 293 } 294 295 $matches = []; 296 $isArray = preg_match("/array\s*<\s*([A-Za-z0-9_|-]+)?(\s*,\s*)?([A-Za-z0-9_|-]+)?\s*>/i", $typeString, $matches); 297 if ($isArray) { 298 if (empty($matches[1]) || empty($matches[3])) { 299 throw new Exception("array<> type hint must have both a key and a value"); 300 } 301 302 return new ArrayType(Type::fromString($matches[1]), Type::fromString($matches[3])); 303 } 304 305 return new SimpleType($typeString, false); 306 } 307 308 /** 309 * @param mixed $value 310 */ 311 public static function fromValue($value): SimpleType 312 { 313 switch (gettype($value)) { 314 case "NULL": 315 return SimpleType::null(); 316 case "boolean": 317 return SimpleType::bool(); 318 case "integer": 319 return SimpleType::int(); 320 case "double": 321 return SimpleType::float(); 322 case "string": 323 return SimpleType::string(); 324 case "array": 325 return SimpleType::array(); 326 case "object": 327 return SimpleType::object(); 328 default: 329 throw new Exception("Type \"" . gettype($value) . "\" cannot be inferred based on value"); 330 } 331 } 332 333 public static function null(): SimpleType 334 { 335 return new SimpleType("null", true); 336 } 337 338 public static function bool(): SimpleType 339 { 340 return new SimpleType("bool", true); 341 } 342 343 public static function int(): SimpleType 344 { 345 return new SimpleType("int", true); 346 } 347 348 public static function float(): SimpleType 349 { 350 return new SimpleType("float", true); 351 } 352 353 public static function string(): SimpleType 354 { 355 return new SimpleType("string", true); 356 } 357 358 public static function array(): SimpleType 359 { 360 return new SimpleType("array", true); 361 } 362 363 public static function object(): SimpleType 364 { 365 return new SimpleType("object", true); 366 } 367 368 public static function void(): SimpleType 369 { 370 return new SimpleType("void", true); 371 } 372 373 protected function __construct(string $name, bool $isBuiltin) { 374 $this->name = $name; 375 $this->isBuiltin = $isBuiltin; 376 } 377 378 public function isScalar(): bool { 379 return $this->isBuiltin && in_array($this->name, ["null", "false", "true", "bool", "int", "float"], true); 380 } 381 382 public function isNull(): bool { 383 return $this->isBuiltin && $this->name === 'null'; 384 } 385 386 public function isBool(): bool { 387 return $this->isBuiltin && $this->name === 'bool'; 388 } 389 390 public function isInt(): bool { 391 return $this->isBuiltin && $this->name === 'int'; 392 } 393 394 public function isFloat(): bool { 395 return $this->isBuiltin && $this->name === 'float'; 396 } 397 398 public function isString(): bool { 399 return $this->isBuiltin && $this->name === 'string'; 400 } 401 402 public function isArray(): bool { 403 return $this->isBuiltin && $this->name === 'array'; 404 } 405 406 public function isMixed(): bool { 407 return $this->isBuiltin && $this->name === 'mixed'; 408 } 409 410 public function toTypeCode(): string { 411 assert($this->isBuiltin); 412 switch ($this->name) { 413 case "bool": 414 return "_IS_BOOL"; 415 case "int": 416 return "IS_LONG"; 417 case "float": 418 return "IS_DOUBLE"; 419 case "string": 420 return "IS_STRING"; 421 case "array": 422 return "IS_ARRAY"; 423 case "object": 424 return "IS_OBJECT"; 425 case "void": 426 return "IS_VOID"; 427 case "callable": 428 return "IS_CALLABLE"; 429 case "mixed": 430 return "IS_MIXED"; 431 case "static": 432 return "IS_STATIC"; 433 case "never": 434 return "IS_NEVER"; 435 case "null": 436 return "IS_NULL"; 437 case "false": 438 return "IS_FALSE"; 439 case "true": 440 return "IS_TRUE"; 441 default: 442 throw new Exception("Not implemented: $this->name"); 443 } 444 } 445 446 public function toTypeMask(): string { 447 assert($this->isBuiltin); 448 449 switch ($this->name) { 450 case "null": 451 return "MAY_BE_NULL"; 452 case "false": 453 return "MAY_BE_FALSE"; 454 case "true": 455 return "MAY_BE_TRUE"; 456 case "bool": 457 return "MAY_BE_BOOL"; 458 case "int": 459 return "MAY_BE_LONG"; 460 case "float": 461 return "MAY_BE_DOUBLE"; 462 case "string": 463 return "MAY_BE_STRING"; 464 case "array": 465 return "MAY_BE_ARRAY"; 466 case "object": 467 return "MAY_BE_OBJECT"; 468 case "callable": 469 return "MAY_BE_CALLABLE"; 470 case "mixed": 471 return "MAY_BE_ANY"; 472 case "void": 473 return "MAY_BE_VOID"; 474 case "static": 475 return "MAY_BE_STATIC"; 476 case "never": 477 return "MAY_BE_NEVER"; 478 default: 479 throw new Exception("Not implemented: $this->name"); 480 } 481 } 482 483 public function toOptimizerTypeMaskForArrayKey(): string { 484 assert($this->isBuiltin); 485 486 switch ($this->name) { 487 case "int": 488 return "MAY_BE_ARRAY_KEY_LONG"; 489 case "string": 490 return "MAY_BE_ARRAY_KEY_STRING"; 491 default: 492 throw new Exception("Type $this->name cannot be an array key"); 493 } 494 } 495 496 public function toOptimizerTypeMaskForArrayValue(): string { 497 if (!$this->isBuiltin) { 498 return "MAY_BE_ARRAY_OF_OBJECT"; 499 } 500 501 switch ($this->name) { 502 case "null": 503 return "MAY_BE_ARRAY_OF_NULL"; 504 case "false": 505 return "MAY_BE_ARRAY_OF_FALSE"; 506 case "true": 507 return "MAY_BE_ARRAY_OF_TRUE"; 508 case "bool": 509 return "MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE"; 510 case "int": 511 return "MAY_BE_ARRAY_OF_LONG"; 512 case "float": 513 return "MAY_BE_ARRAY_OF_DOUBLE"; 514 case "string": 515 return "MAY_BE_ARRAY_OF_STRING"; 516 case "array": 517 return "MAY_BE_ARRAY_OF_ARRAY"; 518 case "object": 519 return "MAY_BE_ARRAY_OF_OBJECT"; 520 case "resource": 521 return "MAY_BE_ARRAY_OF_RESOURCE"; 522 case "mixed": 523 return "MAY_BE_ARRAY_OF_ANY"; 524 case "ref": 525 return "MAY_BE_ARRAY_OF_REF"; 526 default: 527 throw new Exception("Type $this->name cannot be an array value"); 528 } 529 } 530 531 public function toOptimizerTypeMask(): string { 532 if (!$this->isBuiltin) { 533 return "MAY_BE_OBJECT"; 534 } 535 536 switch ($this->name) { 537 case "resource": 538 return "MAY_BE_RESOURCE"; 539 case "callable": 540 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"; 541 case "iterable": 542 return "MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_OBJECT"; 543 case "mixed": 544 return "MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY"; 545 } 546 547 return $this->toTypeMask(); 548 } 549 550 public function toEscapedName(): string { 551 // Escape backslashes, and also encode \u, \U, and \N to avoid compilation errors in generated macros 552 return str_replace( 553 ['\\', '\\u', '\\U', '\\N'], 554 ['\\\\', '\\\\165', '\\\\125', '\\\\116'], 555 $this->name 556 ); 557 } 558 559 public function toVarEscapedName(): string { 560 return str_replace('\\', '_', $this->name); 561 } 562 563 public function equals(SimpleType $other): bool { 564 return $this->name === $other->name && $this->isBuiltin === $other->isBuiltin; 565 } 566} 567 568class Type { 569 /** @var SimpleType[] */ 570 public array $types; 571 public bool $isIntersection; 572 573 public static function fromNode(Node $node): Type { 574 if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) { 575 $nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types); 576 $types = []; 577 foreach ($nestedTypeObjects as $typeObject) { 578 array_push($types, ...$typeObject->types); 579 } 580 return new Type($types, ($node instanceof Node\IntersectionType)); 581 } 582 583 if ($node instanceof Node\NullableType) { 584 return new Type( 585 [ 586 ...Type::fromNode($node->type)->types, 587 SimpleType::null(), 588 ], 589 false 590 ); 591 } 592 593 if ($node instanceof Node\Identifier && $node->toLowerString() === "iterable") { 594 return new Type( 595 [ 596 SimpleType::fromString("Traversable"), 597 ArrayType::createGenericArray(), 598 ], 599 false 600 ); 601 } 602 603 return new Type([SimpleType::fromNode($node)], false); 604 } 605 606 public static function fromString(string $typeString): self { 607 $typeString .= "|"; 608 $simpleTypes = []; 609 $simpleTypeOffset = 0; 610 $inArray = false; 611 $isIntersection = false; 612 613 $typeStringLength = strlen($typeString); 614 for ($i = 0; $i < $typeStringLength; $i++) { 615 $char = $typeString[$i]; 616 617 if ($char === "<") { 618 $inArray = true; 619 continue; 620 } 621 622 if ($char === ">") { 623 $inArray = false; 624 continue; 625 } 626 627 if ($inArray) { 628 continue; 629 } 630 631 if ($char === "|" || $char === "&") { 632 $isIntersection = ($char === "&"); 633 $simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset)); 634 635 $simpleTypes[] = SimpleType::fromString($simpleTypeName); 636 637 $simpleTypeOffset = $i + 1; 638 } 639 } 640 641 return new Type($simpleTypes, $isIntersection); 642 } 643 644 /** 645 * @param SimpleType[] $types 646 */ 647 private function __construct(array $types, bool $isIntersection) { 648 $this->types = $types; 649 $this->isIntersection = $isIntersection; 650 } 651 652 public function isScalar(): bool { 653 foreach ($this->types as $type) { 654 if (!$type->isScalar()) { 655 return false; 656 } 657 } 658 659 return true; 660 } 661 662 public function isNullable(): bool { 663 foreach ($this->types as $type) { 664 if ($type->isNull()) { 665 return true; 666 } 667 } 668 669 return false; 670 } 671 672 public function getWithoutNull(): Type { 673 return new Type( 674 array_values( 675 array_filter( 676 $this->types, 677 function(SimpleType $type) { 678 return !$type->isNull(); 679 } 680 ) 681 ), 682 false 683 ); 684 } 685 686 public function tryToSimpleType(): ?SimpleType { 687 $withoutNull = $this->getWithoutNull(); 688 /* type has only null */ 689 if (count($withoutNull->types) === 0) { 690 return $this->types[0]; 691 } 692 if (count($withoutNull->types) === 1) { 693 return $withoutNull->types[0]; 694 } 695 return null; 696 } 697 698 public function toArginfoType(): ArginfoType { 699 $classTypes = []; 700 $builtinTypes = []; 701 foreach ($this->types as $type) { 702 if ($type->isBuiltin) { 703 $builtinTypes[] = $type; 704 } else { 705 $classTypes[] = $type; 706 } 707 } 708 return new ArginfoType($classTypes, $builtinTypes); 709 } 710 711 public function toOptimizerTypeMask(): string { 712 $optimizerTypes = []; 713 714 foreach ($this->types as $type) { 715 // TODO Support for toOptimizerMask for intersection 716 $optimizerTypes[] = $type->toOptimizerTypeMask(); 717 } 718 719 return implode("|", $optimizerTypes); 720 } 721 722 public function toOptimizerTypeMaskForArrayKey(): string { 723 $typeMasks = []; 724 725 foreach ($this->types as $type) { 726 $typeMasks[] = $type->toOptimizerTypeMaskForArrayKey(); 727 } 728 729 return implode("|", $typeMasks); 730 } 731 732 public function toOptimizerTypeMaskForArrayValue(): string { 733 $typeMasks = []; 734 735 foreach ($this->types as $type) { 736 $typeMasks[] = $type->toOptimizerTypeMaskForArrayValue(); 737 } 738 739 return implode("|", $typeMasks); 740 } 741 742 public function getTypeForDoc(DOMDocument $doc): DOMElement { 743 if (count($this->types) > 1) { 744 $typeSort = $this->isIntersection ? "intersection" : "union"; 745 $typeElement = $doc->createElement('type'); 746 $typeElement->setAttribute("class", $typeSort); 747 748 foreach ($this->types as $type) { 749 $unionTypeElement = $doc->createElement('type', $type->name); 750 $typeElement->appendChild($unionTypeElement); 751 } 752 } else { 753 $type = $this->types[0]; 754 $name = $type->name; 755 756 $typeElement = $doc->createElement('type', $name); 757 } 758 759 return $typeElement; 760 } 761 762 public static function equals(?Type $a, ?Type $b): bool { 763 if ($a === null || $b === null) { 764 return $a === $b; 765 } 766 767 if (count($a->types) !== count($b->types)) { 768 return false; 769 } 770 771 for ($i = 0; $i < count($a->types); $i++) { 772 if (!$a->types[$i]->equals($b->types[$i])) { 773 return false; 774 } 775 } 776 777 return true; 778 } 779 780 public function __toString() { 781 if ($this->types === null) { 782 return 'mixed'; 783 } 784 785 $char = $this->isIntersection ? '&' : '|'; 786 return implode($char, array_map( 787 function ($type) { return $type->name; }, 788 $this->types) 789 ); 790 } 791} 792 793class ArginfoType { 794 /** @var SimpleType[] $classTypes */ 795 public array $classTypes; 796 /** @var SimpleType[] $builtinTypes */ 797 private array $builtinTypes; 798 799 /** 800 * @param SimpleType[] $classTypes 801 * @param SimpleType[] $builtinTypes 802 */ 803 public function __construct(array $classTypes, array $builtinTypes) { 804 $this->classTypes = $classTypes; 805 $this->builtinTypes = $builtinTypes; 806 } 807 808 public function hasClassType(): bool { 809 return !empty($this->classTypes); 810 } 811 812 public function toClassTypeString(): string { 813 return implode('|', array_map(function(SimpleType $type) { 814 return $type->toEscapedName(); 815 }, $this->classTypes)); 816 } 817 818 public function toTypeMask(): string { 819 if (empty($this->builtinTypes)) { 820 return '0'; 821 } 822 return implode('|', array_map(function(SimpleType $type) { 823 return $type->toTypeMask(); 824 }, $this->builtinTypes)); 825 } 826} 827 828class ArgInfo { 829 const SEND_BY_VAL = 0; 830 const SEND_BY_REF = 1; 831 const SEND_PREFER_REF = 2; 832 833 public string $name; 834 public int $sendBy; 835 public bool $isVariadic; 836 public ?Type $type; 837 public ?Type $phpDocType; 838 public ?string $defaultValue; 839 /** @var AttributeInfo[] */ 840 public array $attributes; 841 842 /** 843 * @param AttributeInfo[] $attributes 844 */ 845 public function __construct( 846 string $name, 847 int $sendBy, 848 bool $isVariadic, 849 ?Type $type, 850 ?Type $phpDocType, 851 ?string $defaultValue, 852 array $attributes 853 ) { 854 $this->name = $name; 855 $this->sendBy = $sendBy; 856 $this->isVariadic = $isVariadic; 857 $this->setTypes($type, $phpDocType); 858 $this->defaultValue = $defaultValue; 859 $this->attributes = $attributes; 860 } 861 862 public function equals(ArgInfo $other): bool { 863 return $this->name === $other->name 864 && $this->sendBy === $other->sendBy 865 && $this->isVariadic === $other->isVariadic 866 && Type::equals($this->type, $other->type) 867 && $this->defaultValue === $other->defaultValue; 868 } 869 870 public function getSendByString(): string { 871 switch ($this->sendBy) { 872 case self::SEND_BY_VAL: 873 return "0"; 874 case self::SEND_BY_REF: 875 return "1"; 876 case self::SEND_PREFER_REF: 877 return "ZEND_SEND_PREFER_REF"; 878 } 879 throw new Exception("Invalid sendBy value"); 880 } 881 882 public function getMethodSynopsisType(): Type { 883 if ($this->type) { 884 return $this->type; 885 } 886 887 if ($this->phpDocType) { 888 return $this->phpDocType; 889 } 890 891 throw new Exception("A parameter must have a type"); 892 } 893 894 public function hasProperDefaultValue(): bool { 895 return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN"; 896 } 897 898 public function getDefaultValueAsArginfoString(): string { 899 if ($this->hasProperDefaultValue()) { 900 return '"' . addslashes($this->defaultValue) . '"'; 901 } 902 903 return "NULL"; 904 } 905 906 public function getDefaultValueAsMethodSynopsisString(): ?string { 907 if ($this->defaultValue === null) { 908 return null; 909 } 910 911 switch ($this->defaultValue) { 912 case 'UNKNOWN': 913 return null; 914 case 'false': 915 case 'true': 916 case 'null': 917 return "&{$this->defaultValue};"; 918 } 919 920 return $this->defaultValue; 921 } 922 923 private function setTypes(?Type $type, ?Type $phpDocType): void 924 { 925 $this->type = $type; 926 $this->phpDocType = $phpDocType; 927 } 928} 929 930interface VariableLikeName { 931 public function __toString(): string; 932 public function getDeclarationName(): string; 933} 934 935interface ConstOrClassConstName extends VariableLikeName { 936 public function equals(ConstOrClassConstName $const): bool; 937 public function isClassConst(): bool; 938 public function isUnknown(): bool; 939} 940 941abstract class AbstractConstName implements ConstOrClassConstName 942{ 943 public function equals(ConstOrClassConstName $const): bool 944 { 945 return $this->__toString() === $const->__toString(); 946 } 947 948 public function isUnknown(): bool 949 { 950 return strtolower($this->__toString()) === "unknown"; 951 } 952} 953 954class ConstName extends AbstractConstName { 955 public string $const; 956 957 public function __construct(?Name $namespace, string $const) 958 { 959 if ($namespace && ($namespace = $namespace->slice(0, -1))) { 960 $const = $namespace->toString() . '\\' . $const; 961 } 962 $this->const = $const; 963 } 964 965 public function isClassConst(): bool 966 { 967 return false; 968 } 969 970 public function isUnknown(): bool 971 { 972 $name = $this->__toString(); 973 if (($pos = strrpos($name, '\\')) !== false) { 974 $name = substr($name, $pos + 1); 975 } 976 return strtolower($name) === "unknown"; 977 } 978 979 public function __toString(): string 980 { 981 return $this->const; 982 } 983 984 public function getDeclarationName(): string 985 { 986 return $this->name->toString(); 987 } 988} 989 990class ClassConstName extends AbstractConstName { 991 public Name $class; 992 public string $const; 993 994 public function __construct(Name $class, string $const) 995 { 996 $this->class = $class; 997 $this->const = $const; 998 } 999 1000 public function isClassConst(): bool 1001 { 1002 return true; 1003 } 1004 1005 public function __toString(): string 1006 { 1007 return $this->class->toString() . "::" . $this->const; 1008 } 1009 1010 public function getDeclarationName(): string 1011 { 1012 return $this->const; 1013 } 1014} 1015 1016class PropertyName implements VariableLikeName { 1017 public Name $class; 1018 public string $property; 1019 1020 public function __construct(Name $class, string $property) 1021 { 1022 $this->class = $class; 1023 $this->property = $property; 1024 } 1025 1026 public function __toString(): string 1027 { 1028 return $this->class->toString() . "::$" . $this->property; 1029 } 1030 1031 public function getDeclarationName(): string 1032 { 1033 return $this->property; 1034 } 1035} 1036 1037interface FunctionOrMethodName { 1038 public function getDeclaration(): string; 1039 public function getArgInfoName(): string; 1040 public function getMethodSynopsisFilename(): string; 1041 public function getNameForAttributes(): string; 1042 public function __toString(): string; 1043 public function isMethod(): bool; 1044 public function isConstructor(): bool; 1045 public function isDestructor(): bool; 1046} 1047 1048class FunctionName implements FunctionOrMethodName { 1049 private Name $name; 1050 1051 public function __construct(Name $name) { 1052 $this->name = $name; 1053 } 1054 1055 public function getNamespace(): ?string { 1056 if ($this->name->isQualified()) { 1057 return $this->name->slice(0, -1)->toString(); 1058 } 1059 return null; 1060 } 1061 1062 public function getNonNamespacedName(): string { 1063 if ($this->name->isQualified()) { 1064 throw new Exception("Namespaced name not supported here"); 1065 } 1066 return $this->name->toString(); 1067 } 1068 1069 public function getDeclarationName(): string { 1070 return implode('_', $this->name->getParts()); 1071 } 1072 1073 public function getFunctionName(): string { 1074 return $this->name->getLast(); 1075 } 1076 1077 public function getDeclaration(): string { 1078 return "ZEND_FUNCTION({$this->getDeclarationName()});\n"; 1079 } 1080 1081 public function getArgInfoName(): string { 1082 $underscoreName = implode('_', $this->name->getParts()); 1083 return "arginfo_$underscoreName"; 1084 } 1085 1086 public function getFramelessFunctionInfosName(): string { 1087 $underscoreName = implode('_', $this->name->getParts()); 1088 return "frameless_function_infos_$underscoreName"; 1089 } 1090 1091 public function getMethodSynopsisFilename(): string { 1092 return 'functions/' . implode('/', str_replace('_', '-', $this->name->getParts())); 1093 } 1094 1095 public function getNameForAttributes(): string { 1096 return strtolower($this->name->toString()); 1097 } 1098 1099 public function __toString(): string { 1100 return $this->name->toString(); 1101 } 1102 1103 public function isMethod(): bool { 1104 return false; 1105 } 1106 1107 public function isConstructor(): bool { 1108 return false; 1109 } 1110 1111 public function isDestructor(): bool { 1112 return false; 1113 } 1114} 1115 1116class MethodName implements FunctionOrMethodName { 1117 public Name $className; 1118 public string $methodName; 1119 1120 public function __construct(Name $className, string $methodName) { 1121 $this->className = $className; 1122 $this->methodName = $methodName; 1123 } 1124 1125 public function getDeclarationClassName(): string { 1126 return implode('_', $this->className->getParts()); 1127 } 1128 1129 public function getDeclaration(): string { 1130 return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; 1131 } 1132 1133 public function getArgInfoName(): string { 1134 return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; 1135 } 1136 1137 public function getMethodSynopsisFilename(): string 1138 { 1139 $parts = [...$this->className->getParts(), ltrim($this->methodName, '_')]; 1140 /* File paths are in lowercase */ 1141 return strtolower(implode('/', $parts)); 1142 } 1143 1144 public function getNameForAttributes(): string { 1145 return strtolower($this->methodName); 1146 } 1147 1148 public function __toString(): string { 1149 return "$this->className::$this->methodName"; 1150 } 1151 1152 public function isMethod(): bool { 1153 return true; 1154 } 1155 1156 public function isConstructor(): bool { 1157 return $this->methodName === "__construct"; 1158 } 1159 1160 public function isDestructor(): bool { 1161 return $this->methodName === "__destruct"; 1162 } 1163} 1164 1165class ReturnInfo { 1166 const REFCOUNT_0 = "0"; 1167 const REFCOUNT_1 = "1"; 1168 const REFCOUNT_N = "N"; 1169 1170 const REFCOUNTS = [ 1171 self::REFCOUNT_0, 1172 self::REFCOUNT_1, 1173 self::REFCOUNT_N, 1174 ]; 1175 1176 public bool $byRef; 1177 public ?Type $type; 1178 public ?Type $phpDocType; 1179 public bool $tentativeReturnType; 1180 public string $refcount; 1181 1182 public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType, bool $tentativeReturnType, ?string $refcount) { 1183 $this->byRef = $byRef; 1184 $this->setTypes($type, $phpDocType, $tentativeReturnType); 1185 $this->setRefcount($refcount); 1186 } 1187 1188 public function equalsApartFromPhpDocAndRefcount(ReturnInfo $other): bool { 1189 return $this->byRef === $other->byRef 1190 && Type::equals($this->type, $other->type) 1191 && $this->tentativeReturnType === $other->tentativeReturnType; 1192 } 1193 1194 public function getMethodSynopsisType(): ?Type { 1195 return $this->type ?? $this->phpDocType; 1196 } 1197 1198 private function setTypes(?Type $type, ?Type $phpDocType, bool $tentativeReturnType): void 1199 { 1200 $this->type = $type; 1201 $this->phpDocType = $phpDocType; 1202 $this->tentativeReturnType = $tentativeReturnType; 1203 } 1204 1205 private function setRefcount(?string $refcount): void 1206 { 1207 $type = $this->phpDocType ?? $this->type; 1208 $isScalarType = $type !== null && $type->isScalar(); 1209 1210 if ($refcount === null) { 1211 $this->refcount = $isScalarType ? self::REFCOUNT_0 : self::REFCOUNT_N; 1212 return; 1213 } 1214 1215 if (!in_array($refcount, ReturnInfo::REFCOUNTS, true)) { 1216 throw new Exception("@refcount must have one of the following values: \"0\", \"1\", \"N\", $refcount given"); 1217 } 1218 1219 if ($isScalarType && $refcount !== self::REFCOUNT_0) { 1220 throw new Exception('A scalar return type of "' . $type->__toString() . '" must have a refcount of "' . self::REFCOUNT_0 . '"'); 1221 } 1222 1223 if (!$isScalarType && $refcount === self::REFCOUNT_0) { 1224 throw new Exception('A non-scalar return type of "' . $type->__toString() . '" cannot have a refcount of "' . self::REFCOUNT_0 . '"'); 1225 } 1226 1227 $this->refcount = $refcount; 1228 } 1229} 1230 1231class FuncInfo { 1232 public FunctionOrMethodName $name; 1233 public int $classFlags; 1234 public int $flags; 1235 public ?string $aliasType; 1236 public ?FunctionOrMethodName $alias; 1237 public bool $isDeprecated; 1238 public bool $supportsCompileTimeEval; 1239 public bool $verify; 1240 /** @var ArgInfo[] */ 1241 public array $args; 1242 public ReturnInfo $return; 1243 public int $numRequiredArgs; 1244 public ?string $cond; 1245 public bool $isUndocumentable; 1246 public ?int $minimumPhpVersionIdCompatibility; 1247 /** @var AttributeInfo[] */ 1248 public array $attributes; 1249 /** @var FramelessFunctionInfo[] */ 1250 public array $framelessFunctionInfos; 1251 public ?ExposedDocComment $exposedDocComment; 1252 1253 /** 1254 * @param ArgInfo[] $args 1255 * @param AttributeInfo[] $attribute 1256 * @param FramelessFunctionInfo[] $framelessFunctionInfos 1257 */ 1258 public function __construct( 1259 FunctionOrMethodName $name, 1260 int $classFlags, 1261 int $flags, 1262 ?string $aliasType, 1263 ?FunctionOrMethodName $alias, 1264 bool $isDeprecated, 1265 bool $supportsCompileTimeEval, 1266 bool $verify, 1267 array $args, 1268 ReturnInfo $return, 1269 int $numRequiredArgs, 1270 ?string $cond, 1271 bool $isUndocumentable, 1272 ?int $minimumPhpVersionIdCompatibility, 1273 array $attributes, 1274 array $framelessFunctionInfos, 1275 ?ExposedDocComment $exposedDocComment 1276 ) { 1277 $this->name = $name; 1278 $this->classFlags = $classFlags; 1279 $this->flags = $flags; 1280 $this->aliasType = $aliasType; 1281 $this->alias = $alias; 1282 $this->isDeprecated = $isDeprecated; 1283 $this->supportsCompileTimeEval = $supportsCompileTimeEval; 1284 $this->verify = $verify; 1285 $this->args = $args; 1286 $this->return = $return; 1287 $this->numRequiredArgs = $numRequiredArgs; 1288 $this->cond = $cond; 1289 $this->isUndocumentable = $isUndocumentable; 1290 $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; 1291 $this->attributes = $attributes; 1292 $this->framelessFunctionInfos = $framelessFunctionInfos; 1293 $this->exposedDocComment = $exposedDocComment; 1294 } 1295 1296 public function isMethod(): bool 1297 { 1298 return $this->name->isMethod(); 1299 } 1300 1301 public function isFinalMethod(): bool 1302 { 1303 return ($this->flags & Modifiers::FINAL) || ($this->classFlags & Modifiers::FINAL); 1304 } 1305 1306 public function isInstanceMethod(): bool 1307 { 1308 return !($this->flags & Modifiers::STATIC) && $this->isMethod() && !$this->name->isConstructor(); 1309 } 1310 1311 /** @return string[] */ 1312 public function getModifierNames(): array 1313 { 1314 if (!$this->isMethod()) { 1315 return []; 1316 } 1317 1318 $result = []; 1319 1320 if ($this->flags & Modifiers::FINAL) { 1321 $result[] = "final"; 1322 } elseif ($this->flags & Modifiers::ABSTRACT && $this->classFlags & ~Modifiers::ABSTRACT) { 1323 $result[] = "abstract"; 1324 } 1325 1326 if ($this->flags & Modifiers::PROTECTED) { 1327 $result[] = "protected"; 1328 } elseif ($this->flags & Modifiers::PRIVATE) { 1329 $result[] = "private"; 1330 } else { 1331 $result[] = "public"; 1332 } 1333 1334 if ($this->flags & Modifiers::STATIC) { 1335 $result[] = "static"; 1336 } 1337 1338 return $result; 1339 } 1340 1341 public function hasParamWithUnknownDefaultValue(): bool 1342 { 1343 foreach ($this->args as $arg) { 1344 if ($arg->defaultValue && !$arg->hasProperDefaultValue()) { 1345 return true; 1346 } 1347 } 1348 1349 return false; 1350 } 1351 1352 public function equalsApartFromNameAndRefcount(FuncInfo $other): bool { 1353 if (count($this->args) !== count($other->args)) { 1354 return false; 1355 } 1356 1357 for ($i = 0; $i < count($this->args); $i++) { 1358 if (!$this->args[$i]->equals($other->args[$i])) { 1359 return false; 1360 } 1361 } 1362 1363 return $this->return->equalsApartFromPhpDocAndRefcount($other->return) 1364 && $this->numRequiredArgs === $other->numRequiredArgs 1365 && $this->cond === $other->cond; 1366 } 1367 1368 public function getArgInfoName(): string { 1369 return $this->name->getArgInfoName(); 1370 } 1371 1372 public function getDeclarationKey(): string 1373 { 1374 $name = $this->alias ?? $this->name; 1375 1376 return "$name|$this->cond"; 1377 } 1378 1379 public function getDeclaration(): ?string 1380 { 1381 if ($this->flags & Modifiers::ABSTRACT) { 1382 return null; 1383 } 1384 1385 $name = $this->alias ?? $this->name; 1386 1387 return $name->getDeclaration(); 1388 } 1389 1390 public function getFramelessDeclaration(FuncInfo $funcInfo): ?string { 1391 if (empty($this->framelessFunctionInfos)) { 1392 return null; 1393 } 1394 1395 $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; 1396 1397 $code = ''; 1398 1399 if (!$php84MinimumCompatibility) { 1400 $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; 1401 } 1402 1403 foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { 1404 $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity});\n"; 1405 } 1406 1407 $code .= 'static const zend_frameless_function_info ' . $this->getFramelessFunctionInfosName() . "[] = {\n"; 1408 foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) { 1409 $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n"; 1410 } 1411 $code .= "\t{ 0 },\n"; 1412 $code .= "};\n"; 1413 1414 if (!$php84MinimumCompatibility) { 1415 $code .= "#endif\n"; 1416 } 1417 1418 return $code; 1419 } 1420 1421 public function getFramelessFunctionInfosName(): string { 1422 return $this->name->getFramelessFunctionInfosName(); 1423 } 1424 1425 public function getFunctionEntry(): string { 1426 $code = ""; 1427 1428 $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID; 1429 $isVanillaEntry = $this->alias === null && !$this->supportsCompileTimeEval && $this->exposedDocComment === null && empty($this->framelessFunctionInfos); 1430 $argInfoName = $this->getArgInfoName(); 1431 $flagsByPhpVersions = $this->getArginfoFlagsByPhpVersions(); 1432 $functionEntryCode = null; 1433 1434 if (!empty($this->framelessFunctionInfos)) { 1435 if ($this->isMethod()) { 1436 throw new Exception('Frameless methods are not supported yet'); 1437 } 1438 if ($this->name->getNamespace()) { 1439 throw new Exception('Namespaced direct calls to frameless functions are not supported yet'); 1440 } 1441 if ($this->alias) { 1442 throw new Exception('Aliased direct calls to frameless functions are not supported yet'); 1443 } 1444 } 1445 1446 if ($this->isMethod()) { 1447 $zendName = '"' . $this->name->methodName . '"'; 1448 if ($this->alias) { 1449 if ($this->alias instanceof MethodName) { 1450 $name = "zim_" . $this->alias->getDeclarationClassName() . "_" . $this->alias->methodName; 1451 } else if ($this->alias instanceof FunctionName) { 1452 $name = "zif_" . $this->alias->getNonNamespacedName(); 1453 } else { 1454 throw new Error("Cannot happen"); 1455 } 1456 } else { 1457 if ($this->flags & Modifiers::ABSTRACT) { 1458 $name = "NULL"; 1459 } else { 1460 $name = "zim_" . $this->name->getDeclarationClassName() . "_" . $this->name->methodName; 1461 1462 if ($isVanillaEntry) { 1463 $functionEntryCode = "\tZEND_ME(" . $this->name->getDeclarationClassName() . ", " . $this->name->methodName . ", $argInfoName, " . implode("|", reset($flagsByPhpVersions)) . ")"; 1464 } 1465 } 1466 } 1467 } else if ($this->name instanceof FunctionName) { 1468 $functionName = $this->name->getFunctionName(); 1469 $declarationName = $this->alias ? $this->alias->getNonNamespacedName() : $this->name->getDeclarationName(); 1470 1471 if ($this->name->getNamespace()) { 1472 $namespace = addslashes($this->name->getNamespace()); 1473 $zendName = "ZEND_NS_NAME(\"$namespace\", \"$functionName\")"; 1474 $name = "zif_$declarationName"; 1475 } else { 1476 $zendName = '"' . $functionName . '"'; 1477 $name = "zif_$declarationName"; 1478 1479 if ($isVanillaEntry && reset($flagsByPhpVersions) === ["0"]) { 1480 $functionEntryCode = "\tZEND_FE($declarationName, $argInfoName)"; 1481 } 1482 } 1483 } else { 1484 throw new Error("Cannot happen"); 1485 } 1486 1487 if ($functionEntryCode !== null) { 1488 $code .= "$functionEntryCode\n"; 1489 } else { 1490 if (!$php84MinimumCompatibility) { 1491 $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; 1492 } 1493 1494 $php84AndAboveFlags = array_slice($flagsByPhpVersions, 5, null, true); 1495 $docComment = $this->exposedDocComment ? '"' . $this->exposedDocComment->escape() . '"' : "NULL"; 1496 $framelessFuncInfosName = !empty($this->framelessFunctionInfos) ? $this->getFramelessFunctionInfosName() : "NULL"; 1497 1498 $template = "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s, $framelessFuncInfosName, $docComment)\n"; 1499 $flagsCode = generateVersionDependentFlagCode( 1500 $template, 1501 $php84AndAboveFlags, 1502 PHP_84_VERSION_ID 1503 ); 1504 $code .= implode("", $flagsCode); 1505 1506 if (!$php84MinimumCompatibility) { 1507 $code .= "#else\n"; 1508 } 1509 1510 if (!$php84MinimumCompatibility) { 1511 $flags = array_slice($flagsByPhpVersions, 0, 4, true); 1512 $template = "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s)\n"; 1513 $flagsCode = generateVersionDependentFlagCode( 1514 $template, 1515 $flags, 1516 $this->minimumPhpVersionIdCompatibility 1517 ); 1518 $code .= implode("", $flagsCode); 1519 } 1520 1521 if (!$php84MinimumCompatibility) { 1522 $code .= "#endif\n"; 1523 } 1524 } 1525 1526 return $code; 1527 } 1528 1529 public function getOptimizerInfo(): ?string { 1530 if ($this->isMethod()) { 1531 return null; 1532 } 1533 1534 if ($this->alias !== null) { 1535 return null; 1536 } 1537 1538 if ($this->return->refcount !== ReturnInfo::REFCOUNT_1 && $this->return->phpDocType === null) { 1539 return null; 1540 } 1541 1542 $type = $this->return->phpDocType ?? $this->return->type; 1543 if ($type === null) { 1544 return null; 1545 } 1546 1547 return "\tF" . $this->return->refcount . '("' . addslashes($this->name->__toString()) . '", ' . $type->toOptimizerTypeMask() . "),\n"; 1548 } 1549 1550 public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void { 1551 $this->attributes = []; 1552 $this->return->type = null; 1553 $this->framelessFunctionInfos = []; 1554 $this->exposedDocComment = null; 1555 $this->supportsCompileTimeEval = false; 1556 foreach ($this->args as $arg) { 1557 $arg->type = null; 1558 $arg->defaultValue = null; 1559 $arg->attributes = []; 1560 } 1561 $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; 1562 } 1563 1564 /** @return array<int, string[]> */ 1565 private function getArginfoFlagsByPhpVersions(): array 1566 { 1567 $flags = []; 1568 1569 if ($this->isMethod()) { 1570 if ($this->flags & Modifiers::PROTECTED) { 1571 $flags[] = "ZEND_ACC_PROTECTED"; 1572 } elseif ($this->flags & Modifiers::PRIVATE) { 1573 $flags[] = "ZEND_ACC_PRIVATE"; 1574 } else { 1575 $flags[] = "ZEND_ACC_PUBLIC"; 1576 } 1577 1578 if ($this->flags & Modifiers::STATIC) { 1579 $flags[] = "ZEND_ACC_STATIC"; 1580 } 1581 1582 if ($this->flags & Modifiers::FINAL) { 1583 $flags[] = "ZEND_ACC_FINAL"; 1584 } 1585 1586 if ($this->flags & Modifiers::ABSTRACT) { 1587 $flags[] = "ZEND_ACC_ABSTRACT"; 1588 } 1589 } 1590 1591 if ($this->isDeprecated) { 1592 $flags[] = "ZEND_ACC_DEPRECATED"; 1593 } 1594 1595 foreach ($this->attributes as $attr) { 1596 if ($attr->class === "Deprecated") { 1597 $flags[] = "ZEND_ACC_DEPRECATED"; 1598 break; 1599 } 1600 } 1601 1602 $php82AndAboveFlags = $flags; 1603 if ($this->isMethod() === false && $this->supportsCompileTimeEval) { 1604 $php82AndAboveFlags[] = "ZEND_ACC_COMPILE_TIME_EVAL"; 1605 } 1606 1607 if (empty($flags)) { 1608 $flags[] = "0"; 1609 } 1610 if (empty($php82AndAboveFlags)) { 1611 $php82AndAboveFlags[] = "0"; 1612 } 1613 1614 return [ 1615 PHP_70_VERSION_ID => $flags, 1616 PHP_80_VERSION_ID => $flags, 1617 PHP_81_VERSION_ID => $flags, 1618 PHP_82_VERSION_ID => $php82AndAboveFlags, 1619 PHP_83_VERSION_ID => $php82AndAboveFlags, 1620 PHP_84_VERSION_ID => $php82AndAboveFlags, 1621 ]; 1622 } 1623 1624 private function generateRefSect1(DOMDocument $doc, string $role): DOMElement { 1625 $refSec = $doc->createElement('refsect1'); 1626 $refSec->setAttribute('role', $role); 1627 $refSec->append( 1628 "\n ", 1629 $doc->createEntityReference('reftitle.' . $role), 1630 "\n " 1631 ); 1632 return $refSec; 1633 } 1634 1635 /** 1636 * @param array<string, FuncInfo> $funcMap 1637 * @param array<string, FuncInfo> $aliasMap 1638 * @throws Exception 1639 */ 1640 public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string { 1641 $REFSEC1_SEPERATOR = "\n\n "; 1642 1643 $doc = new DOMDocument("1.0", "utf-8"); 1644 $doc->formatOutput = true; 1645 1646 $refentry = $doc->createElement('refentry'); 1647 $doc->appendChild($refentry); 1648 1649 if ($this->isMethod()) { 1650 assert($this->name instanceof MethodName); 1651 /* Namespaces are seperated by '-', '_' must be converted to '-' too. 1652 * Trim away the __ for magic methods */ 1653 $id = strtolower( 1654 str_replace('\\', '-', $this->name->className->__toString()) 1655 . '.' 1656 . str_replace('_', '-', ltrim($this->name->methodName, '_')) 1657 ); 1658 } else { 1659 $id = 'function.' . strtolower(str_replace('_', '-', $this->name->__toString())); 1660 } 1661 $refentry->setAttribute("xml:id", $id); 1662 /* We create an attribute for xmlns, as libxml otherwise force it to be the first one */ 1663 //$refentry->setAttribute("xmlns", "http://docbook.org/ns/docbook"); 1664 $namespace = $doc->createAttribute('xmlns'); 1665 $namespace->value = "http://docbook.org/ns/docbook"; 1666 $refentry->setAttributeNode($namespace); 1667 $refentry->setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); 1668 $refentry->appendChild(new DOMText("\n ")); 1669 1670 /* Creation of <refnamediv> */ 1671 $refnamediv = $doc->createElement('refnamediv'); 1672 $refnamediv->appendChild(new DOMText("\n ")); 1673 $refname = $doc->createElement('refname', $this->name->__toString()); 1674 $refnamediv->appendChild($refname); 1675 $refnamediv->appendChild(new DOMText("\n ")); 1676 $refpurpose = $doc->createElement('refpurpose', 'Description'); 1677 $refnamediv->appendChild($refpurpose); 1678 1679 $refnamediv->appendChild(new DOMText("\n ")); 1680 $refentry->append($refnamediv, $REFSEC1_SEPERATOR); 1681 1682 /* Creation of <refsect1 role="description"> */ 1683 $descriptionRefSec = $this->generateRefSect1($doc, 'description'); 1684 1685 $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 1686 if (!$methodSynopsis) { 1687 return null; 1688 } 1689 $descriptionRefSec->appendChild($methodSynopsis); 1690 $descriptionRefSec->appendChild(new DOMText("\n ")); 1691 $undocumentedEntity = $doc->createEntityReference('warn.undocumented.func'); 1692 $descriptionRefSec->appendChild($undocumentedEntity); 1693 $descriptionRefSec->appendChild(new DOMText("\n ")); 1694 $returnDescriptionPara = $doc->createElement('para'); 1695 $returnDescriptionPara->appendChild(new DOMText("\n Description.\n ")); 1696 $descriptionRefSec->appendChild($returnDescriptionPara); 1697 1698 $descriptionRefSec->appendChild(new DOMText("\n ")); 1699 $refentry->append($descriptionRefSec, $REFSEC1_SEPERATOR); 1700 1701 /* Creation of <refsect1 role="parameters"> */ 1702 $parametersRefSec = $this->getParameterSection($doc); 1703 $refentry->append($parametersRefSec, $REFSEC1_SEPERATOR); 1704 1705 /* Creation of <refsect1 role="returnvalues"> */ 1706 if (!$this->name->isConstructor() && !$this->name->isDestructor()) { 1707 $returnRefSec = $this->getReturnValueSection($doc); 1708 $refentry->append($returnRefSec, $REFSEC1_SEPERATOR); 1709 } 1710 1711 /* Creation of <refsect1 role="errors"> */ 1712 $errorsRefSec = $this->generateRefSect1($doc, 'errors'); 1713 $errorsDescriptionParaConstantTag = $doc->createElement('constant'); 1714 $errorsDescriptionParaConstantTag->append('E_*'); 1715 $errorsDescriptionParaExceptionTag = $doc->createElement('exceptionname'); 1716 $errorsDescriptionParaExceptionTag->append('Exception'); 1717 $errorsDescriptionPara = $doc->createElement('para'); 1718 $errorsDescriptionPara->append( 1719 "\n When does this function issue ", 1720 $errorsDescriptionParaConstantTag, 1721 " level errors,\n and/or throw ", 1722 $errorsDescriptionParaExceptionTag, 1723 "s.\n " 1724 ); 1725 $errorsRefSec->appendChild($errorsDescriptionPara); 1726 $errorsRefSec->appendChild(new DOMText("\n ")); 1727 1728 $refentry->append($errorsRefSec, $REFSEC1_SEPERATOR); 1729 1730 /* Creation of <refsect1 role="changelog"> */ 1731 $changelogRefSec = $this->getChangelogSection($doc); 1732 $refentry->append($changelogRefSec, $REFSEC1_SEPERATOR); 1733 1734 $exampleRefSec = $this->getExampleSection($doc, $id); 1735 $refentry->append($exampleRefSec, $REFSEC1_SEPERATOR); 1736 1737 /* Creation of <refsect1 role="notes"> */ 1738 $notesRefSec = $this->generateRefSect1($doc, 'notes'); 1739 1740 $noteTagSimara = $doc->createElement('simpara'); 1741 $noteTagSimara->append( 1742 "\n Any notes that don't fit anywhere else should go here.\n " 1743 ); 1744 $noteTag = $doc->createElement('note'); 1745 $noteTag->append("\n ", $noteTagSimara, "\n "); 1746 $notesRefSec->append($noteTag, "\n "); 1747 1748 $refentry->append($notesRefSec, $REFSEC1_SEPERATOR); 1749 1750 /* Creation of <refsect1 role="seealso"> */ 1751 $seeAlsoRefSec = $this->generateRefSect1($doc, 'seealso'); 1752 1753 $seeAlsoMemberClassMethod = $doc->createElement('member'); 1754 $seeAlsoMemberClassMethodTag = $doc->createElement('methodname'); 1755 $seeAlsoMemberClassMethodTag->appendChild(new DOMText("ClassName::otherMethodName")); 1756 $seeAlsoMemberClassMethod->appendChild($seeAlsoMemberClassMethodTag); 1757 1758 $seeAlsoMemberFunction = $doc->createElement('member'); 1759 $seeAlsoMemberFunctionTag = $doc->createElement('function'); 1760 $seeAlsoMemberFunctionTag->appendChild(new DOMText("some_function")); 1761 $seeAlsoMemberFunction->appendChild($seeAlsoMemberFunctionTag); 1762 1763 $seeAlsoMemberLink = $doc->createElement('member'); 1764 $seeAlsoMemberLinkTag = $doc->createElement('link'); 1765 $seeAlsoMemberLinkTag->setAttribute('linkend', 'some.id.chunk.to.link'); 1766 $seeAlsoMemberLinkTag->appendChild(new DOMText('something appendix')); 1767 $seeAlsoMemberLink->appendChild($seeAlsoMemberLinkTag); 1768 1769 $seeAlsoList = $doc->createElement('simplelist'); 1770 $seeAlsoList->append( 1771 "\n ", 1772 $seeAlsoMemberClassMethod, 1773 "\n ", 1774 $seeAlsoMemberFunction, 1775 "\n ", 1776 $seeAlsoMemberLink, 1777 "\n " 1778 ); 1779 1780 $seeAlsoRefSec->appendChild($seeAlsoList); 1781 $seeAlsoRefSec->appendChild(new DOMText("\n ")); 1782 1783 $refentry->appendChild($seeAlsoRefSec); 1784 1785 $refentry->appendChild(new DOMText("\n\n")); 1786 1787 $doc->appendChild(new DOMComment( 1788 <<<ENDCOMMENT 1789 Keep this comment at the end of the file 1790Local variables: 1791mode: sgml 1792sgml-omittag:t 1793sgml-shorttag:t 1794sgml-minimize-attributes:nil 1795sgml-always-quote-attributes:t 1796sgml-indent-step:1 1797sgml-indent-data:t 1798indent-tabs-mode:nil 1799sgml-parent-document:nil 1800sgml-default-dtd-file:"~/.phpdoc/manual.ced" 1801sgml-exposed-tags:nil 1802sgml-local-catalogs:nil 1803sgml-local-ecat-files:nil 1804End: 1805vim600: syn=xml fen fdm=syntax fdl=2 si 1806vim: et tw=78 syn=sgml 1807vi: ts=1 sw=1 1808 1809ENDCOMMENT 1810 )); 1811 return $doc->saveXML(); 1812 } 1813 1814 private function getParameterSection(DOMDocument $doc): DOMElement { 1815 $parametersRefSec = $this->generateRefSect1($doc, 'parameters'); 1816 if (empty($this->args)) { 1817 $noParamEntity = $doc->createEntityReference('no.function.parameters'); 1818 $parametersRefSec->appendChild($noParamEntity); 1819 return $parametersRefSec; 1820 } else { 1821 $parametersPara = $doc->createElement('para'); 1822 $parametersRefSec->appendChild($parametersPara); 1823 1824 $parametersPara->appendChild(new DOMText("\n ")); 1825 $parametersList = $doc->createElement('variablelist'); 1826 $parametersPara->appendChild($parametersList); 1827 1828 /* 1829 <varlistentry> 1830 <term><parameter>name</parameter></term> 1831 <listitem> 1832 <para> 1833 Description. 1834 </para> 1835 </listitem> 1836 </varlistentry> 1837 */ 1838 foreach ($this->args as $arg) { 1839 $parameter = $doc->createElement('parameter', $arg->name); 1840 $parameterTerm = $doc->createElement('term'); 1841 $parameterTerm->appendChild($parameter); 1842 1843 $listItemPara = $doc->createElement('para'); 1844 $listItemPara->append( 1845 "\n ", 1846 "Description.", 1847 "\n ", 1848 ); 1849 1850 $parameterEntryListItem = $doc->createElement('listitem'); 1851 $parameterEntryListItem->append( 1852 "\n ", 1853 $listItemPara, 1854 "\n ", 1855 ); 1856 1857 $parameterEntry = $doc->createElement('varlistentry'); 1858 $parameterEntry->append( 1859 "\n ", 1860 $parameterTerm, 1861 "\n ", 1862 $parameterEntryListItem, 1863 "\n ", 1864 ); 1865 1866 $parametersList->appendChild(new DOMText("\n ")); 1867 $parametersList->appendChild($parameterEntry); 1868 } 1869 $parametersList->appendChild(new DOMText("\n ")); 1870 } 1871 $parametersPara->appendChild(new DOMText("\n ")); 1872 $parametersRefSec->appendChild(new DOMText("\n ")); 1873 return $parametersRefSec; 1874 } 1875 1876 private function getReturnValueSection(DOMDocument $doc): DOMElement { 1877 $returnRefSec = $this->generateRefSect1($doc, 'returnvalues'); 1878 1879 $returnDescriptionPara = $doc->createElement('para'); 1880 $returnDescriptionPara->appendChild(new DOMText("\n ")); 1881 1882 $returnType = $this->return->getMethodSynopsisType(); 1883 if ($returnType === null) { 1884 $returnDescriptionPara->appendChild(new DOMText("Description.")); 1885 } else if (count($returnType->types) === 1) { 1886 $type = $returnType->types[0]; 1887 $name = $type->name; 1888 1889 switch ($name) { 1890 case 'void': 1891 $descriptionNode = $doc->createEntityReference('return.void'); 1892 break; 1893 case 'true': 1894 $descriptionNode = $doc->createEntityReference('return.true.always'); 1895 break; 1896 case 'bool': 1897 $descriptionNode = $doc->createEntityReference('return.success'); 1898 break; 1899 default: 1900 $descriptionNode = new DOMText("Description."); 1901 break; 1902 } 1903 $returnDescriptionPara->appendChild($descriptionNode); 1904 } else { 1905 $returnDescriptionPara->appendChild(new DOMText("Description.")); 1906 } 1907 $returnDescriptionPara->appendChild(new DOMText("\n ")); 1908 $returnRefSec->appendChild($returnDescriptionPara); 1909 $returnRefSec->appendChild(new DOMText("\n ")); 1910 return $returnRefSec; 1911 } 1912 1913 /** 1914 * @param array<DOMNode> $headers [count($headers) === $columns] 1915 * @param array<array<DOMNode>> $rows [count($rows[$i]) === $columns] 1916 */ 1917 private function generateDocbookInformalTable( 1918 DOMDocument $doc, 1919 int $indent, 1920 int $columns, 1921 array $headers, 1922 array $rows 1923 ): DOMElement { 1924 $strIndent = str_repeat(' ', $indent); 1925 1926 $headerRow = $doc->createElement('row'); 1927 foreach ($headers as $header) { 1928 $headerEntry = $doc->createElement('entry'); 1929 $headerEntry->appendChild($header); 1930 1931 $headerRow->append("\n$strIndent ", $headerEntry); 1932 } 1933 $headerRow->append("\n$strIndent "); 1934 1935 $thead = $doc->createElement('thead'); 1936 $thead->append( 1937 "\n$strIndent ", 1938 $headerRow, 1939 "\n$strIndent ", 1940 ); 1941 1942 $tbody = $doc->createElement('tbody'); 1943 foreach ($rows as $row) { 1944 $bodyRow = $doc->createElement('row'); 1945 foreach ($row as $cell) { 1946 $entry = $doc->createElement('entry'); 1947 $entry->appendChild($cell); 1948 1949 $bodyRow->appendChild(new DOMText("\n$strIndent ")); 1950 $bodyRow->appendChild($entry); 1951 } 1952 $bodyRow->appendChild(new DOMText("\n$strIndent ")); 1953 1954 $tbody->append( 1955 "\n$strIndent ", 1956 $bodyRow, 1957 "\n$strIndent ", 1958 ); 1959 } 1960 1961 $tgroup = $doc->createElement('tgroup'); 1962 $tgroup->setAttribute('cols', (string) $columns); 1963 $tgroup->append( 1964 "\n$strIndent ", 1965 $thead, 1966 "\n$strIndent ", 1967 $tbody, 1968 "\n$strIndent ", 1969 ); 1970 1971 $table = $doc->createElement('informaltable'); 1972 $table->append( 1973 "\n$strIndent ", 1974 $tgroup, 1975 "\n$strIndent", 1976 ); 1977 1978 return $table; 1979 } 1980 1981 private function getChangelogSection(DOMDocument $doc): DOMElement { 1982 $refSec = $this->generateRefSect1($doc, 'changelog'); 1983 $headers = [ 1984 $doc->createEntityReference('Version'), 1985 $doc->createEntityReference('Description'), 1986 ]; 1987 $rows = [[ 1988 new DOMText('8.X.0'), 1989 new DOMText("\n Description\n "), 1990 ]]; 1991 $table = $this->generateDocbookInformalTable( 1992 $doc, 1993 /* indent: */ 2, 1994 /* columns: */ 2, 1995 /* headers: */ $headers, 1996 /* rows: */ $rows 1997 ); 1998 $refSec->appendChild($table); 1999 2000 $refSec->appendChild(new DOMText("\n ")); 2001 return $refSec; 2002 } 2003 2004 private function getExampleSection(DOMDocument $doc, string $id): DOMElement { 2005 $refSec = $this->generateRefSect1($doc, 'examples'); 2006 2007 $example = $doc->createElement('example'); 2008 $fnName = $this->name->__toString(); 2009 $example->setAttribute('xml:id', $id . '.example.basic'); 2010 2011 $title = $doc->createElement('title'); 2012 $fn = $doc->createElement($this->isMethod() ? 'methodname' : 'function'); 2013 $fn->append($fnName); 2014 $title->append($fn, ' example'); 2015 2016 $example->append("\n ", $title); 2017 2018 $para = $doc->createElement('para'); 2019 $para->append("\n ", "Description.", "\n "); 2020 $example->append("\n ", $para); 2021 2022 $prog = $doc->createElement('programlisting'); 2023 $prog->setAttribute('role', 'php'); 2024 $code = new DOMCdataSection( 2025 <<<CODE_EXAMPLE 2026 2027<?php 2028echo "Code example"; 2029?> 2030 2031CODE_EXAMPLE 2032 ); 2033 $prog->append("\n"); 2034 $prog->appendChild($code); 2035 $prog->append("\n "); 2036 2037 $example->append("\n ", $prog); 2038 $example->append("\n ", $doc->createEntityReference('example.outputs')); 2039 2040 $output = new DOMCdataSection( 2041 <<<OUPUT_EXAMPLE 2042 2043Code example 2044 2045OUPUT_EXAMPLE 2046 ); 2047 $screen = $doc->createElement('screen'); 2048 $screen->append("\n"); 2049 $screen->appendChild($output); 2050 $screen->append("\n "); 2051 2052 $example->append( 2053 "\n ", 2054 $screen, 2055 "\n ", 2056 ); 2057 2058 $refSec->append( 2059 $example, 2060 "\n ", 2061 ); 2062 return $refSec; 2063 } 2064 2065 /** 2066 * @param array<string, FuncInfo> $funcMap 2067 * @param array<string, FuncInfo> $aliasMap 2068 * @throws Exception 2069 */ 2070 public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement { 2071 if ($this->hasParamWithUnknownDefaultValue()) { 2072 return null; 2073 } 2074 2075 if ($this->name->isConstructor()) { 2076 $synopsisType = "constructorsynopsis"; 2077 } elseif ($this->name->isDestructor()) { 2078 $synopsisType = "destructorsynopsis"; 2079 } else { 2080 $synopsisType = "methodsynopsis"; 2081 } 2082 2083 $methodSynopsis = $doc->createElement($synopsisType); 2084 2085 if ($this->isMethod()) { 2086 assert($this->name instanceof MethodName); 2087 $role = $doc->createAttribute("role"); 2088 $role->value = addslashes($this->name->className->__toString()); 2089 $methodSynopsis->appendChild($role); 2090 } 2091 2092 $methodSynopsis->appendChild(new DOMText("\n ")); 2093 2094 foreach ($this->getModifierNames() as $modifierString) { 2095 $modifierElement = $doc->createElement('modifier', $modifierString); 2096 $methodSynopsis->appendChild($modifierElement); 2097 $methodSynopsis->appendChild(new DOMText(" ")); 2098 } 2099 2100 $returnType = $this->return->getMethodSynopsisType(); 2101 if ($returnType) { 2102 $methodSynopsis->appendChild($returnType->getTypeForDoc($doc)); 2103 } 2104 2105 $methodname = $doc->createElement('methodname', $this->name->__toString()); 2106 $methodSynopsis->appendChild($methodname); 2107 2108 if (empty($this->args)) { 2109 $methodSynopsis->appendChild(new DOMText("\n ")); 2110 $void = $doc->createElement('void'); 2111 $methodSynopsis->appendChild($void); 2112 } else { 2113 foreach ($this->args as $arg) { 2114 $methodSynopsis->appendChild(new DOMText("\n ")); 2115 $methodparam = $doc->createElement('methodparam'); 2116 if ($arg->defaultValue !== null) { 2117 $methodparam->setAttribute("choice", "opt"); 2118 } 2119 if ($arg->isVariadic) { 2120 $methodparam->setAttribute("rep", "repeat"); 2121 } 2122 2123 $methodSynopsis->appendChild($methodparam); 2124 foreach ($arg->attributes as $attribute) { 2125 $attribute = $doc->createElement("modifier", "#[\\" . $attribute->class . "]"); 2126 $attribute->setAttribute("role", "attribute"); 2127 2128 $methodparam->appendChild($attribute); 2129 } 2130 2131 $methodparam->appendChild($arg->getMethodSynopsisType()->getTypeForDoc($doc)); 2132 2133 $parameter = $doc->createElement('parameter', $arg->name); 2134 if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) { 2135 $parameter->setAttribute("role", "reference"); 2136 } 2137 2138 $methodparam->appendChild($parameter); 2139 $defaultValue = $arg->getDefaultValueAsMethodSynopsisString(); 2140 if ($defaultValue !== null) { 2141 $initializer = $doc->createElement('initializer'); 2142 if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) { 2143 $constant = $doc->createElement('constant', $defaultValue); 2144 $initializer->appendChild($constant); 2145 } else { 2146 $initializer->nodeValue = $defaultValue; 2147 } 2148 $methodparam->appendChild($initializer); 2149 } 2150 } 2151 } 2152 $methodSynopsis->appendChild(new DOMText("\n ")); 2153 2154 return $methodSynopsis; 2155 } 2156 2157 public function __clone() 2158 { 2159 foreach ($this->args as $key => $argInfo) { 2160 $this->args[$key] = clone $argInfo; 2161 } 2162 $this->return = clone $this->return; 2163 foreach ($this->attributes as $key => $attribute) { 2164 $this->attributes[$key] = clone $attribute; 2165 } 2166 foreach ($this->framelessFunctionInfos as $key => $framelessFunctionInfo) { 2167 $this->framelessFunctionInfos[$key] = clone $framelessFunctionInfo; 2168 } 2169 if ($this->exposedDocComment) { 2170 $this->exposedDocComment = clone $this->exposedDocComment; 2171 } 2172 } 2173} 2174 2175class EvaluatedValue 2176{ 2177 /** @var mixed */ 2178 public $value; 2179 public SimpleType $type; 2180 public Expr $expr; 2181 public bool $isUnknownConstValue; 2182 /** @var ConstInfo[] */ 2183 public array $originatingConsts; 2184 2185 /** 2186 * @param array<string, ConstInfo> $allConstInfos 2187 */ 2188 public static function createFromExpression(Expr $expr, ?SimpleType $constType, ?string $cConstName, array $allConstInfos): EvaluatedValue 2189 { 2190 // This visitor replaces the PHP constants by C constants. It allows direct expansion of the compiled constants, e.g. later in the pretty printer. 2191 $visitor = new class($allConstInfos) extends PhpParser\NodeVisitorAbstract 2192 { 2193 /** @var iterable<ConstInfo> */ 2194 public array $visitedConstants = []; 2195 /** @var array<string, ConstInfo> */ 2196 public array $allConstInfos; 2197 2198 /** @param array<string, ConstInfo> $allConstInfos */ 2199 public function __construct(array $allConstInfos) 2200 { 2201 $this->allConstInfos = $allConstInfos; 2202 } 2203 2204 /** @return Node|null */ 2205 public function enterNode(Node $expr) 2206 { 2207 if (!$expr instanceof Expr\ConstFetch && !$expr instanceof Expr\ClassConstFetch) { 2208 return null; 2209 } 2210 2211 if ($expr instanceof Expr\ClassConstFetch) { 2212 $originatingConstName = new ClassConstName($expr->class, $expr->name->toString()); 2213 } else { 2214 $originatingConstName = new ConstName($expr->name->getAttribute('namespacedName'), $expr->name->toString()); 2215 } 2216 2217 if ($originatingConstName->isUnknown()) { 2218 return null; 2219 } 2220 2221 $const = $this->allConstInfos[$originatingConstName->__toString()] ?? null; 2222 if ($const !== null) { 2223 $this->visitedConstants[] = $const; 2224 return $const->getValue($this->allConstInfos)->expr; 2225 } 2226 } 2227 }; 2228 2229 $nodeTraverser = new PhpParser\NodeTraverser; 2230 $nodeTraverser->addVisitor($visitor); 2231 $expr = $nodeTraverser->traverse([$expr])[0]; 2232 2233 $isUnknownConstValue = false; 2234 2235 $evaluator = new ConstExprEvaluator( 2236 static function (Expr $expr) use ($allConstInfos, &$isUnknownConstValue) { 2237 // $expr is a ConstFetch with a name of a C macro here 2238 if (!$expr instanceof Expr\ConstFetch) { 2239 throw new Exception($this->getVariableTypeName() . " " . $this->name->__toString() . " has an unsupported value"); 2240 } 2241 2242 $constName = $expr->name->__toString(); 2243 if (strtolower($constName) === "unknown") { 2244 $isUnknownConstValue = true; 2245 return null; 2246 } 2247 2248 foreach ($allConstInfos as $const) { 2249 if ($constName != $const->cValue) { 2250 continue; 2251 } 2252 2253 $constType = ($const->phpDocType ?? $const->type)->tryToSimpleType(); 2254 if ($constType) { 2255 if ($constType->isBool()) { 2256 return true; 2257 } elseif ($constType->isInt()) { 2258 return 1; 2259 } elseif ($constType->isFloat()) { 2260 return M_PI; 2261 } elseif ($constType->isString()) { 2262 return $const->name; 2263 } elseif ($constType->isArray()) { 2264 return []; 2265 } 2266 } 2267 2268 return null; 2269 } 2270 2271 throw new Exception("Constant " . $constName . " cannot be found"); 2272 } 2273 ); 2274 2275 $result = $evaluator->evaluateDirectly($expr); 2276 2277 return new EvaluatedValue( 2278 $result, // note: we are generally not interested in the actual value of $result, unless it's a bare value, without constants 2279 $constType ?? SimpleType::fromValue($result), 2280 $cConstName === null ? $expr : new Expr\ConstFetch(new Node\Name($cConstName)), 2281 $visitor->visitedConstants, 2282 $isUnknownConstValue 2283 ); 2284 } 2285 2286 public static function null(): EvaluatedValue 2287 { 2288 return new self(null, SimpleType::null(), new Expr\ConstFetch(new Node\Name('null')), [], false); 2289 } 2290 2291 /** 2292 * @param mixed $value 2293 * @param ConstInfo[] $originatingConsts 2294 */ 2295 private function __construct($value, SimpleType $type, Expr $expr, array $originatingConsts, bool $isUnknownConstValue) 2296 { 2297 $this->value = $value; 2298 $this->type = $type; 2299 $this->expr = $expr; 2300 $this->originatingConsts = $originatingConsts; 2301 $this->isUnknownConstValue = $isUnknownConstValue; 2302 } 2303 2304 public function initializeZval(string $zvalName): string 2305 { 2306 $cExpr = $this->getCExpr(); 2307 2308 $code = "\tzval $zvalName;\n"; 2309 2310 if ($this->type->isNull()) { 2311 $code .= "\tZVAL_NULL(&$zvalName);\n"; 2312 } elseif ($this->type->isBool()) { 2313 if ($cExpr == 'true') { 2314 $code .= "\tZVAL_TRUE(&$zvalName);\n"; 2315 } elseif ($cExpr == 'false') { 2316 $code .= "\tZVAL_FALSE(&$zvalName);\n"; 2317 } else { 2318 $code .= "\tZVAL_BOOL(&$zvalName, $cExpr);\n"; 2319 } 2320 } elseif ($this->type->isInt()) { 2321 $code .= "\tZVAL_LONG(&$zvalName, $cExpr);\n"; 2322 } elseif ($this->type->isFloat()) { 2323 $code .= "\tZVAL_DOUBLE(&$zvalName, $cExpr);\n"; 2324 } elseif ($this->type->isString()) { 2325 if ($cExpr === '""') { 2326 $code .= "\tZVAL_EMPTY_STRING(&$zvalName);\n"; 2327 } else { 2328 $code .= "\tzend_string *{$zvalName}_str = zend_string_init($cExpr, strlen($cExpr), 1);\n"; 2329 $code .= "\tZVAL_STR(&$zvalName, {$zvalName}_str);\n"; 2330 } 2331 } elseif ($this->type->isArray()) { 2332 if ($cExpr == '[]') { 2333 $code .= "\tZVAL_EMPTY_ARRAY(&$zvalName);\n"; 2334 } else { 2335 throw new Exception("Unimplemented default value"); 2336 } 2337 } else { 2338 throw new Exception("Invalid default value: " . print_r($this->value, true) . ", type: " . print_r($this->type, true)); 2339 } 2340 2341 return $code; 2342 } 2343 2344 public function getCExpr(): ?string 2345 { 2346 // $this->expr has all its PHP constants replaced by C constants 2347 $prettyPrinter = new Standard; 2348 $expr = $prettyPrinter->prettyPrintExpr($this->expr); 2349 // PHP single-quote to C double-quote string 2350 if ($this->type->isString()) { 2351 $expr = preg_replace("/(^'|'$)/", '"', $expr); 2352 } 2353 return $expr[0] == '"' ? $expr : preg_replace('(\bnull\b)', 'NULL', str_replace('\\', '', $expr)); 2354 } 2355} 2356 2357abstract class VariableLike 2358{ 2359 public int $flags; 2360 public ?Type $type; 2361 public ?Type $phpDocType; 2362 public ?string $link; 2363 public ?int $phpVersionIdMinimumCompatibility; 2364 /** @var AttributeInfo[] */ 2365 public array $attributes; 2366 public ?ExposedDocComment $exposedDocComment; 2367 2368 /** 2369 * @var AttributeInfo[] $attributes 2370 */ 2371 public function __construct( 2372 int $flags, 2373 ?Type $type, 2374 ?Type $phpDocType, 2375 ?string $link, 2376 ?int $phpVersionIdMinimumCompatibility, 2377 array $attributes, 2378 ?ExposedDocComment $exposedDocComment 2379 ) { 2380 $this->flags = $flags; 2381 $this->type = $type; 2382 $this->phpDocType = $phpDocType; 2383 $this->link = $link; 2384 $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; 2385 $this->attributes = $attributes; 2386 $this->exposedDocComment = $exposedDocComment; 2387 } 2388 2389 abstract protected function getVariableTypeCode(): string; 2390 2391 abstract protected function getVariableTypeName(): string; 2392 2393 abstract protected function getFieldSynopsisDefaultLinkend(): string; 2394 2395 abstract protected function getFieldSynopsisName(): string; 2396 2397 /** @param array<string, ConstInfo> $allConstInfos */ 2398 abstract protected function getFieldSynopsisValueString(array $allConstInfos): ?string; 2399 2400 abstract public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void; 2401 2402 protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2403 { 2404 $type = $this->phpDocType ?? $this->type; 2405 2406 if ($type) { 2407 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2408 $fieldsynopsisElement->appendChild($type->getTypeForDoc($doc)); 2409 } 2410 } 2411 2412 /** 2413 * @return array<int, string[]> 2414 */ 2415 protected function getFlagsByPhpVersion(): array 2416 { 2417 $flags = "ZEND_ACC_PUBLIC"; 2418 if ($this->flags & Modifiers::PROTECTED) { 2419 $flags = "ZEND_ACC_PROTECTED"; 2420 } elseif ($this->flags & Modifiers::PRIVATE) { 2421 $flags = "ZEND_ACC_PRIVATE"; 2422 } 2423 2424 return [ 2425 PHP_70_VERSION_ID => [$flags], 2426 PHP_80_VERSION_ID => [$flags], 2427 PHP_81_VERSION_ID => [$flags], 2428 PHP_82_VERSION_ID => [$flags], 2429 PHP_83_VERSION_ID => [$flags], 2430 PHP_84_VERSION_ID => [$flags], 2431 ]; 2432 } 2433 2434 protected function getTypeCode(string $variableLikeName, string &$code): string 2435 { 2436 $variableLikeType = $this->getVariableTypeName(); 2437 2438 $typeCode = ""; 2439 if ($this->type) { 2440 $arginfoType = $this->type->toArginfoType(); 2441 if ($arginfoType->hasClassType()) { 2442 if (count($arginfoType->classTypes) >= 2) { 2443 foreach ($arginfoType->classTypes as $classType) { 2444 $escapedClassName = $classType->toEscapedName(); 2445 $varEscapedClassName = $classType->toVarEscapedName(); 2446 $code .= "\tzend_string *{$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\") - 1, 1);\n"; 2447 } 2448 2449 $classTypeCount = count($arginfoType->classTypes); 2450 $code .= "\tzend_type_list *{$variableLikeType}_{$variableLikeName}_type_list = malloc(ZEND_TYPE_LIST_SIZE($classTypeCount));\n"; 2451 $code .= "\t{$variableLikeType}_{$variableLikeName}_type_list->num_types = $classTypeCount;\n"; 2452 2453 foreach ($arginfoType->classTypes as $k => $classType) { 2454 $escapedClassName = $classType->toEscapedName(); 2455 $code .= "\t{$variableLikeType}_{$variableLikeName}_type_list->types[$k] = (zend_type) ZEND_TYPE_INIT_CLASS({$variableLikeType}_{$variableLikeName}_class_{$escapedClassName}, 0, 0);\n"; 2456 } 2457 2458 $typeMaskCode = $this->type->toArginfoType()->toTypeMask(); 2459 2460 if ($this->type->isIntersection) { 2461 $code .= "\tzend_type {$variableLikeType}_{$variableLikeName}_type = ZEND_TYPE_INIT_INTERSECTION({$variableLikeType}_{$variableLikeName}_type_list, $typeMaskCode);\n"; 2462 } else { 2463 $code .= "\tzend_type {$variableLikeType}_{$variableLikeName}_type = ZEND_TYPE_INIT_UNION({$variableLikeType}_{$variableLikeName}_type_list, $typeMaskCode);\n"; 2464 } 2465 $typeCode = "{$variableLikeType}_{$variableLikeName}_type"; 2466 } else { 2467 $escapedClassName = $arginfoType->classTypes[0]->toEscapedName(); 2468 $varEscapedClassName = $arginfoType->classTypes[0]->toVarEscapedName(); 2469 $code .= "\tzend_string *{$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\")-1, 1);\n"; 2470 2471 $typeCode = "(zend_type) ZEND_TYPE_INIT_CLASS({$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName}, 0, " . $arginfoType->toTypeMask() . ")"; 2472 } 2473 } else { 2474 $typeCode = "(zend_type) ZEND_TYPE_INIT_MASK(" . $arginfoType->toTypeMask() . ")"; 2475 } 2476 } else { 2477 $typeCode = "(zend_type) ZEND_TYPE_INIT_NONE(0)"; 2478 } 2479 2480 return $typeCode; 2481 } 2482 2483 /** @param array<string, ConstInfo> $allConstInfos */ 2484 public function getFieldSynopsisElement(DOMDocument $doc, array $allConstInfos): DOMElement 2485 { 2486 $fieldsynopsisElement = $doc->createElement("fieldsynopsis"); 2487 2488 $this->addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 2489 2490 $this->addTypeToFieldSynopsis($doc, $fieldsynopsisElement); 2491 2492 $varnameElement = $doc->createElement("varname", $this->getFieldSynopsisName()); 2493 if ($this->link) { 2494 $varnameElement->setAttribute("linkend", $this->link); 2495 } else { 2496 $varnameElement->setAttribute("linkend", $this->getFieldSynopsisDefaultLinkend()); 2497 } 2498 2499 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2500 $fieldsynopsisElement->appendChild($varnameElement); 2501 2502 $valueString = $this->getFieldSynopsisValueString($allConstInfos); 2503 if ($valueString) { 2504 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2505 $initializerElement = $doc->createElement("initializer", $valueString); 2506 $fieldsynopsisElement->appendChild($initializerElement); 2507 } 2508 2509 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2510 2511 return $fieldsynopsisElement; 2512 } 2513 2514 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2515 { 2516 if ($this->flags & Modifiers::PUBLIC) { 2517 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2518 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "public")); 2519 } elseif ($this->flags & Modifiers::PROTECTED) { 2520 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2521 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected")); 2522 } elseif ($this->flags & Modifiers::PRIVATE) { 2523 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2524 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private")); 2525 } 2526 } 2527 2528 /** 2529 * @param array<int, string[]> $flags 2530 * @return array<int, string[]> 2531 */ 2532 protected function addFlagForVersionsAbove(array $flags, string $flag, int $minimumVersionId): array 2533 { 2534 $write = false; 2535 2536 foreach ($flags as $version => $versionFlags) { 2537 if ($version === $minimumVersionId || $write === true) { 2538 $flags[$version][] = $flag; 2539 $write = true; 2540 } 2541 } 2542 2543 return $flags; 2544 } 2545} 2546 2547class ConstInfo extends VariableLike 2548{ 2549 public ConstOrClassConstName $name; 2550 public Expr $value; 2551 public bool $isDeprecated; 2552 public ?string $valueString; 2553 public ?string $cond; 2554 public ?string $cValue; 2555 public bool $isUndocumentable; 2556 public bool $isFileCacheAllowed; 2557 2558 /** 2559 * @var AttributeInfo[] $attributes 2560 */ 2561 public function __construct( 2562 ConstOrClassConstName $name, 2563 int $flags, 2564 Expr $value, 2565 ?string $valueString, 2566 ?Type $type, 2567 ?Type $phpDocType, 2568 bool $isDeprecated, 2569 ?string $cond, 2570 ?string $cValue, 2571 bool $isUndocumentable, 2572 ?string $link, 2573 ?int $phpVersionIdMinimumCompatibility, 2574 array $attributes, 2575 ?ExposedDocComment $exposedDocComment, 2576 bool $isFileCacheAllowed 2577 ) { 2578 $this->name = $name; 2579 $this->value = $value; 2580 $this->valueString = $valueString; 2581 $this->isDeprecated = $isDeprecated; 2582 $this->cond = $cond; 2583 $this->cValue = $cValue; 2584 $this->isUndocumentable = $isUndocumentable; 2585 $this->isFileCacheAllowed = $isFileCacheAllowed; 2586 parent::__construct($flags, $type, $phpDocType, $link, $phpVersionIdMinimumCompatibility, $attributes, $exposedDocComment); 2587 } 2588 2589 /** @param array<string, ConstInfo> $allConstInfos */ 2590 public function getValue(array $allConstInfos): EvaluatedValue 2591 { 2592 return EvaluatedValue::createFromExpression( 2593 $this->value, 2594 ($this->phpDocType ?? $this->type)->tryToSimpleType(), 2595 $this->cValue, 2596 $allConstInfos 2597 ); 2598 } 2599 2600 protected function getVariableTypeName(): string 2601 { 2602 return "constant"; 2603 } 2604 2605 protected function getVariableTypeCode(): string 2606 { 2607 return "const"; 2608 } 2609 2610 protected function getFieldSynopsisDefaultLinkend(): string 2611 { 2612 $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); 2613 2614 return "$className.constants." . strtolower(str_replace(["__", "_"], ["", "-"], $this->name->getDeclarationName())); 2615 } 2616 2617 protected function getFieldSynopsisName(): string 2618 { 2619 return $this->name->__toString(); 2620 } 2621 2622 /** @param array<string, ConstInfo> $allConstInfos */ 2623 protected function getFieldSynopsisValueString(array $allConstInfos): ?string 2624 { 2625 $value = EvaluatedValue::createFromExpression($this->value, null, $this->cValue, $allConstInfos); 2626 if ($value->isUnknownConstValue) { 2627 return null; 2628 } 2629 2630 if ($value->originatingConsts) { 2631 return implode("\n", array_map(function (ConstInfo $const) use ($allConstInfos) { 2632 return $const->getFieldSynopsisValueString($allConstInfos); 2633 }, $value->originatingConsts)); 2634 } 2635 2636 return $this->valueString; 2637 } 2638 2639 public function getPredefinedConstantTerm(DOMDocument $doc, int $indentationLevel): DOMElement { 2640 $indentation = str_repeat(" ", $indentationLevel); 2641 2642 $termElement = $doc->createElement("term"); 2643 2644 $constantElement = $doc->createElement("constant"); 2645 $constantElement->textContent = $this->name->__toString(); 2646 2647 $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc); 2648 2649 $termElement->appendChild(new DOMText("\n$indentation ")); 2650 $termElement->appendChild($constantElement); 2651 $termElement->appendChild(new DOMText("\n$indentation (")); 2652 $termElement->appendChild($typeElement); 2653 $termElement->appendChild(new DOMText(")\n$indentation")); 2654 2655 return $termElement; 2656 } 2657 2658 public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLevel): DOMElement { 2659 $indentation = str_repeat(" ", $indentationLevel); 2660 2661 $entryElement = $doc->createElement("entry"); 2662 2663 $constantElement = $doc->createElement("constant"); 2664 $constantElement->textContent = $this->name->__toString(); 2665 $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc); 2666 2667 $entryElement->appendChild(new DOMText("\n$indentation ")); 2668 $entryElement->appendChild($constantElement); 2669 $entryElement->appendChild(new DOMText("\n$indentation (")); 2670 $entryElement->appendChild($typeElement); 2671 $entryElement->appendChild(new DOMText(")\n$indentation")); 2672 2673 return $entryElement; 2674 } 2675 2676 public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { 2677 $this->type = null; 2678 $this->flags &= ~Modifiers::FINAL; 2679 $this->isDeprecated = false; 2680 $this->attributes = []; 2681 $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; 2682 } 2683 2684 /** @param array<string, ConstInfo> $allConstInfos */ 2685 public function getDeclaration(array $allConstInfos): string 2686 { 2687 $type = $this->phpDocType ?? $this->type; 2688 $simpleType = $type ? $type->tryToSimpleType() : null; 2689 if ($simpleType && $simpleType->name === "mixed") { 2690 $simpleType = null; 2691 } 2692 2693 $value = EvaluatedValue::createFromExpression($this->value, $simpleType, $this->cValue, $allConstInfos); 2694 if ($value->isUnknownConstValue && ($simpleType === null || !$simpleType->isBuiltin)) { 2695 throw new Exception("Constant " . $this->name->__toString() . " must have a built-in PHPDoc type as the type couldn't be inferred from its value"); 2696 } 2697 2698 // i.e. const NAME = UNKNOWN;, without the annotation 2699 if ($value->isUnknownConstValue && $this->cValue === null && $value->expr instanceof Expr\ConstFetch && $value->expr->name->__toString() === "UNKNOWN") { 2700 throw new Exception("Constant " . $this->name->__toString() . " must have a @cvalue annotation"); 2701 } 2702 2703 $code = ""; 2704 2705 if ($this->cond) { 2706 $code .= "#if {$this->cond}\n"; 2707 } 2708 2709 if ($this->name->isClassConst()) { 2710 $code .= $this->getClassConstDeclaration($value, $allConstInfos); 2711 } else { 2712 $code .= $this->getGlobalConstDeclaration($value, $allConstInfos); 2713 } 2714 $code .= $this->getValueAssertion($value); 2715 2716 if ($this->cond) { 2717 $code .= "#endif\n"; 2718 } 2719 2720 return $code; 2721 } 2722 2723 /** @param array<string, ConstInfo> $allConstInfos */ 2724 private function getGlobalConstDeclaration(EvaluatedValue $value, array $allConstInfos): string 2725 { 2726 $constName = str_replace('\\', '\\\\', $this->name->__toString()); 2727 $constValue = $value->value; 2728 $cExpr = $value->getCExpr(); 2729 2730 $flags = "CONST_PERSISTENT"; 2731 if (!$this->isFileCacheAllowed) { 2732 $flags .= " | CONST_NO_FILE_CACHE"; 2733 } 2734 if ($this->phpVersionIdMinimumCompatibility !== null && $this->phpVersionIdMinimumCompatibility < 80000) { 2735 $flags .= " | CONST_CS"; 2736 } 2737 2738 if ($this->isDeprecated) { 2739 $flags .= " | CONST_DEPRECATED"; 2740 } 2741 if ($value->type->isNull()) { 2742 return "\tREGISTER_NULL_CONSTANT(\"$constName\", $flags);\n"; 2743 } 2744 2745 if ($value->type->isBool()) { 2746 return "\tREGISTER_BOOL_CONSTANT(\"$constName\", " . ($cExpr ?: ($constValue ? "true" : "false")) . ", $flags);\n"; 2747 } 2748 2749 if ($value->type->isInt()) { 2750 return "\tREGISTER_LONG_CONSTANT(\"$constName\", " . ($cExpr ?: (int) $constValue) . ", $flags);\n"; 2751 } 2752 2753 if ($value->type->isFloat()) { 2754 return "\tREGISTER_DOUBLE_CONSTANT(\"$constName\", " . ($cExpr ?: (float) $constValue) . ", $flags);\n"; 2755 } 2756 2757 if ($value->type->isString()) { 2758 return "\tREGISTER_STRING_CONSTANT(\"$constName\", " . ($cExpr ?: '"' . addslashes($constValue) . '"') . ", $flags);\n"; 2759 } 2760 2761 throw new Exception("Unimplemented constant type"); 2762 } 2763 2764 /** @param array<string, ConstInfo> $allConstInfos */ 2765 private function getClassConstDeclaration(EvaluatedValue $value, array $allConstInfos): string 2766 { 2767 $constName = $this->name->getDeclarationName(); 2768 2769 $zvalCode = $value->initializeZval("const_{$constName}_value", $allConstInfos); 2770 2771 $code = "\n" . $zvalCode; 2772 2773 $code .= "\tzend_string *const_{$constName}_name = zend_string_init_interned(\"$constName\", sizeof(\"$constName\") - 1, 1);\n"; 2774 $nameCode = "const_{$constName}_name"; 2775 2776 if ($this->exposedDocComment) { 2777 $commentCode = "const_{$constName}_comment"; 2778 $escapedComment = $this->exposedDocComment->escape(); 2779 $escapedCommentLength = $this->exposedDocComment->getLength(); 2780 $code .= "\tzend_string *$commentCode = zend_string_init_interned(\"$escapedComment\", $escapedCommentLength, 1);\n"; 2781 } else { 2782 $commentCode = "NULL"; 2783 } 2784 2785 $php83MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_83_VERSION_ID; 2786 2787 if ($this->type && !$php83MinimumCompatibility) { 2788 $code .= "#if (PHP_VERSION_ID >= " . PHP_83_VERSION_ID . ")\n"; 2789 } 2790 2791 if ($this->type) { 2792 $typeCode = $this->getTypeCode($constName, $code); 2793 2794 if (!empty($this->attributes)) { 2795 $template = "\tzend_class_constant *const_" . $this->name->getDeclarationName() . " = "; 2796 } else { 2797 $template = "\t"; 2798 } 2799 $template .= "zend_declare_typed_class_constant(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode, $typeCode);\n"; 2800 2801 $flagsCode = generateVersionDependentFlagCode( 2802 $template, 2803 $this->getFlagsByPhpVersion(), 2804 $this->phpVersionIdMinimumCompatibility 2805 ); 2806 $code .= implode("", $flagsCode); 2807 } 2808 2809 if ($this->type && !$php83MinimumCompatibility) { 2810 $code .= "#else\n"; 2811 } 2812 2813 if (!$this->type || !$php83MinimumCompatibility) { 2814 if (!empty($this->attributes)) { 2815 $template = "\tzend_class_constant *const_" . $this->name->getDeclarationName() . " = "; 2816 } else { 2817 $template = "\t"; 2818 } 2819 $template .= "zend_declare_class_constant_ex(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode);\n"; 2820 $flagsCode = generateVersionDependentFlagCode( 2821 $template, 2822 $this->getFlagsByPhpVersion(), 2823 $this->phpVersionIdMinimumCompatibility 2824 ); 2825 $code .= implode("", $flagsCode); 2826 } 2827 2828 if ($this->type && !$php83MinimumCompatibility) { 2829 $code .= "#endif\n"; 2830 } 2831 2832 $code .= "\tzend_string_release(const_{$constName}_name);\n"; 2833 2834 return $code; 2835 } 2836 2837 private function getValueAssertion(EvaluatedValue $value): string 2838 { 2839 if ($value->isUnknownConstValue || $value->originatingConsts || $this->cValue === null) { 2840 return ""; 2841 } 2842 2843 $cExpr = $value->getCExpr(); 2844 $constValue = $value->value; 2845 2846 if ($value->type->isNull()) { 2847 return "\tZEND_ASSERT($cExpr == NULL);\n"; 2848 } 2849 2850 if ($value->type->isBool()) { 2851 $cValue = $constValue ? "true" : "false"; 2852 return "\tZEND_ASSERT($cExpr == $cValue);\n"; 2853 } 2854 2855 if ($value->type->isInt()) { 2856 $cValue = (int) $constValue; 2857 return "\tZEND_ASSERT($cExpr == $cValue);\n"; 2858 } 2859 2860 if ($value->type->isFloat()) { 2861 $cValue = (float) $constValue; 2862 return "\tZEND_ASSERT($cExpr == $cValue);\n"; 2863 } 2864 2865 if ($value->type->isString()) { 2866 $cValue = '"' . addslashes($constValue) . '"'; 2867 return "\tZEND_ASSERT(strcmp($cExpr, $cValue) == 0);\n"; 2868 } 2869 2870 throw new Exception("Unimplemented constant type"); 2871 } 2872 2873 /** 2874 * @return array<int, string[]> 2875 */ 2876 protected function getFlagsByPhpVersion(): array 2877 { 2878 $flags = parent::getFlagsByPhpVersion(); 2879 2880 if ($this->isDeprecated) { 2881 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID); 2882 } 2883 2884 foreach ($this->attributes as $attr) { 2885 if ($attr->class === "Deprecated") { 2886 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID); 2887 break; 2888 } 2889 } 2890 2891 if ($this->flags & Modifiers::FINAL) { 2892 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_FINAL", PHP_81_VERSION_ID); 2893 } 2894 2895 return $flags; 2896 } 2897 2898 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 2899 { 2900 parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 2901 2902 if ($this->flags & Modifiers::FINAL) { 2903 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2904 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "final")); 2905 } 2906 2907 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 2908 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "const")); 2909 } 2910} 2911 2912class PropertyInfo extends VariableLike 2913{ 2914 public int $classFlags; 2915 public PropertyName $name; 2916 public ?Expr $defaultValue; 2917 public ?string $defaultValueString; 2918 public bool $isDocReadonly; 2919 public bool $isVirtual; 2920 2921 /** 2922 * @var AttributeInfo[] $attributes 2923 */ 2924 public function __construct( 2925 PropertyName $name, 2926 int $classFlags, 2927 int $flags, 2928 ?Type $type, 2929 ?Type $phpDocType, 2930 ?Expr $defaultValue, 2931 ?string $defaultValueString, 2932 bool $isDocReadonly, 2933 bool $isVirtual, 2934 ?string $link, 2935 ?int $phpVersionIdMinimumCompatibility, 2936 array $attributes, 2937 ?ExposedDocComment $exposedDocComment 2938 ) { 2939 $this->name = $name; 2940 $this->classFlags = $classFlags; 2941 $this->defaultValue = $defaultValue; 2942 $this->defaultValueString = $defaultValueString; 2943 $this->isDocReadonly = $isDocReadonly; 2944 $this->isVirtual = $isVirtual; 2945 parent::__construct($flags, $type, $phpDocType, $link, $phpVersionIdMinimumCompatibility, $attributes, $exposedDocComment); 2946 } 2947 2948 protected function getVariableTypeCode(): string 2949 { 2950 return "property"; 2951 } 2952 2953 protected function getVariableTypeName(): string 2954 { 2955 return "property"; 2956 } 2957 2958 protected function getFieldSynopsisDefaultLinkend(): string 2959 { 2960 $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); 2961 2962 return "$className.props." . strtolower(str_replace(["__", "_"], ["", "-"], $this->name->getDeclarationName())); 2963 } 2964 2965 protected function getFieldSynopsisName(): string 2966 { 2967 return $this->name->getDeclarationName(); 2968 } 2969 2970 /** @param array<string, ConstInfo> $allConstInfos */ 2971 protected function getFieldSynopsisValueString(array $allConstInfos): ?string 2972 { 2973 return $this->defaultValueString; 2974 } 2975 2976 public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { 2977 $this->type = null; 2978 $this->flags &= ~Modifiers::READONLY; 2979 $this->attributes = []; 2980 $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; 2981 } 2982 2983 /** @param array<string, ConstInfo> $allConstInfos */ 2984 public function getDeclaration(array $allConstInfos): string { 2985 $code = "\n"; 2986 2987 $propertyName = $this->name->getDeclarationName(); 2988 2989 if ($this->defaultValue === null) { 2990 $defaultValue = EvaluatedValue::null(); 2991 } else { 2992 $defaultValue = EvaluatedValue::createFromExpression($this->defaultValue, null, null, $allConstInfos); 2993 if ($defaultValue->isUnknownConstValue || ($defaultValue->originatingConsts && $defaultValue->getCExpr() === null)) { 2994 echo "Skipping code generation for property $this->name, because it has an unknown constant default value\n"; 2995 return ""; 2996 } 2997 } 2998 2999 $zvalName = "property_{$propertyName}_default_value"; 3000 if ($this->defaultValue === null && $this->type !== null) { 3001 $code .= "\tzval $zvalName;\n\tZVAL_UNDEF(&$zvalName);\n"; 3002 } else { 3003 $code .= $defaultValue->initializeZval($zvalName); 3004 } 3005 3006 $code .= "\tzend_string *property_{$propertyName}_name = zend_string_init(\"$propertyName\", sizeof(\"$propertyName\") - 1, 1);\n"; 3007 $nameCode = "property_{$propertyName}_name"; 3008 3009 if ($this->exposedDocComment) { 3010 $commentCode = "property_{$propertyName}_comment"; 3011 $escapedComment = $this->exposedDocComment->escape(); 3012 $escapedCommentLength = $this->exposedDocComment->getLength(); 3013 $code .= "\tzend_string *$commentCode = zend_string_init_interned(\"$escapedComment\", $escapedCommentLength, 1);\n"; 3014 } else { 3015 $commentCode = "NULL"; 3016 } 3017 3018 if (!empty($this->attributes)) { 3019 $template = "\tzend_property_info *property_" . $this->name->getDeclarationName() . " = "; 3020 } else { 3021 $template = "\t"; 3022 } 3023 3024 if ($this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID) { 3025 $typeCode = $this->getTypeCode($propertyName, $code); 3026 $template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n"; 3027 } else { 3028 $template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n"; 3029 } 3030 3031 $flagsCode = generateVersionDependentFlagCode( 3032 $template, 3033 $this->getFlagsByPhpVersion(), 3034 $this->phpVersionIdMinimumCompatibility 3035 ); 3036 $code .= implode("", $flagsCode); 3037 3038 $code .= "\tzend_string_release(property_{$propertyName}_name);\n"; 3039 3040 return $code; 3041 } 3042 3043 /** 3044 * @return array<int, string[]> 3045 */ 3046 protected function getFlagsByPhpVersion(): array 3047 { 3048 $flags = parent::getFlagsByPhpVersion(); 3049 3050 if ($this->flags & Modifiers::STATIC) { 3051 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_STATIC", PHP_70_VERSION_ID); 3052 } 3053 3054 if ($this->flags & Modifiers::READONLY) { 3055 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_81_VERSION_ID); 3056 } elseif ($this->classFlags & Modifiers::READONLY) { 3057 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_82_VERSION_ID); 3058 } 3059 3060 if ($this->isVirtual) { 3061 $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_VIRTUAL", PHP_84_VERSION_ID); 3062 } 3063 3064 return $flags; 3065 } 3066 3067 protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void 3068 { 3069 parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement); 3070 3071 if ($this->flags & Modifiers::STATIC) { 3072 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 3073 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "static")); 3074 } 3075 3076 if ($this->flags & Modifiers::READONLY || $this->isDocReadonly) { 3077 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 3078 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "readonly")); 3079 } 3080 } 3081 3082 public function __clone() 3083 { 3084 if ($this->type) { 3085 $this->type = clone $this->type; 3086 } 3087 foreach ($this->attributes as $key => $attribute) { 3088 $this->attributes[$key] = clone $attribute; 3089 } 3090 if ($this->exposedDocComment) { 3091 $this->exposedDocComment = clone $this->exposedDocComment; 3092 } 3093 } 3094} 3095 3096class EnumCaseInfo { 3097 public string $name; 3098 public ?Expr $value; 3099 3100 public function __construct(string $name, ?Expr $value) { 3101 $this->name = $name; 3102 $this->value = $value; 3103 } 3104 3105 /** @param array<string, ConstInfo> $allConstInfos */ 3106 public function getDeclaration(array $allConstInfos): string { 3107 $escapedName = addslashes($this->name); 3108 if ($this->value === null) { 3109 $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; 3110 } else { 3111 $value = EvaluatedValue::createFromExpression($this->value, null, null, $allConstInfos); 3112 3113 $zvalName = "enum_case_{$escapedName}_value"; 3114 $code = "\n" . $value->initializeZval($zvalName); 3115 $code .= "\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", &$zvalName);\n"; 3116 } 3117 3118 return $code; 3119 } 3120} 3121 3122class AttributeInfo { 3123 public string $class; 3124 /** @var \PhpParser\Node\Arg[] */ 3125 public array $args; 3126 3127 /** @param \PhpParser\Node\Arg[] $args */ 3128 public function __construct(string $class, array $args) { 3129 $this->class = $class; 3130 $this->args = $args; 3131 } 3132 3133 /** @param array<string, ConstInfo> $allConstInfos */ 3134 public function generateCode(string $invocation, string $nameSuffix, array $allConstInfos, ?int $phpVersionIdMinimumCompatibility): string { 3135 $php82MinimumCompatibility = $phpVersionIdMinimumCompatibility === null || $phpVersionIdMinimumCompatibility >= PHP_82_VERSION_ID; 3136 $php84MinimumCompatibility = $phpVersionIdMinimumCompatibility === null || $phpVersionIdMinimumCompatibility >= PHP_84_VERSION_ID; 3137 /* see ZEND_KNOWN_STRINGS in Zend/strings.h */ 3138 $knowns = [ 3139 "message" => "ZEND_STR_MESSAGE", 3140 ]; 3141 if ($php82MinimumCompatibility) { 3142 $knowns["SensitiveParameter"] = "ZEND_STR_SENSITIVEPARAMETER"; 3143 } 3144 if ($php84MinimumCompatibility) { 3145 $knowns["Deprecated"] = "ZEND_STR_DEPRECATED_CAPITALIZED"; 3146 $knowns["since"] = "ZEND_STR_SINCE"; 3147 } 3148 3149 $code = "\n"; 3150 $escapedAttributeName = strtr($this->class, '\\', '_'); 3151 if (isset($knowns[$escapedAttributeName])) { 3152 $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, ZSTR_KNOWN({$knowns[$escapedAttributeName]}), " . count($this->args) . ");\n"; 3153 } else { 3154 $code .= "\tzend_string *attribute_name_{$escapedAttributeName}_$nameSuffix = zend_string_init_interned(\"" . addcslashes($this->class, "\\") . "\", sizeof(\"" . addcslashes($this->class, "\\") . "\") - 1, 1);\n"; 3155 $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, attribute_name_{$escapedAttributeName}_$nameSuffix, " . count($this->args) . ");\n"; 3156 $code .= "\tzend_string_release(attribute_name_{$escapedAttributeName}_$nameSuffix);\n"; 3157 } 3158 foreach ($this->args as $i => $arg) { 3159 $value = EvaluatedValue::createFromExpression($arg->value, null, null, $allConstInfos); 3160 $zvalName = "attribute_{$escapedAttributeName}_{$nameSuffix}_arg$i"; 3161 $code .= $value->initializeZval($zvalName); 3162 $code .= "\tZVAL_COPY_VALUE(&attribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].value, &$zvalName);\n"; 3163 if ($arg->name) { 3164 if (isset($knowns[$arg->name->name])) { 3165 $code .= "\tattribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].name = ZSTR_KNOWN({$knowns[$arg->name->name]});\n"; 3166 } else { 3167 $code .= "\tattribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].name = zend_string_init_interned(\"{$arg->name->name}\", sizeof(\"{$arg->name->name}\") - 1, 1);\n"; 3168 } 3169 } 3170 } 3171 return $code; 3172 } 3173} 3174 3175class ClassInfo { 3176 public Name $name; 3177 public int $flags; 3178 public string $type; 3179 public ?string $alias; 3180 public ?SimpleType $enumBackingType; 3181 public bool $isDeprecated; 3182 public bool $isStrictProperties; 3183 /** @var AttributeInfo[] */ 3184 public array $attributes; 3185 public ?ExposedDocComment $exposedDocComment; 3186 public bool $isNotSerializable; 3187 /** @var Name[] */ 3188 public array $extends; 3189 /** @var Name[] */ 3190 public array $implements; 3191 /** @var ConstInfo[] */ 3192 public array $constInfos; 3193 /** @var PropertyInfo[] */ 3194 public array $propertyInfos; 3195 /** @var FuncInfo[] */ 3196 public array $funcInfos; 3197 /** @var EnumCaseInfo[] */ 3198 public array $enumCaseInfos; 3199 public ?string $cond; 3200 public ?int $phpVersionIdMinimumCompatibility; 3201 public bool $isUndocumentable; 3202 3203 /** 3204 * @param AttributeInfo[] $attributes 3205 * @param Name[] $extends 3206 * @param Name[] $implements 3207 * @param ConstInfo[] $constInfos 3208 * @param PropertyInfo[] $propertyInfos 3209 * @param FuncInfo[] $funcInfos 3210 * @param EnumCaseInfo[] $enumCaseInfos 3211 */ 3212 public function __construct( 3213 Name $name, 3214 int $flags, 3215 string $type, 3216 ?string $alias, 3217 ?SimpleType $enumBackingType, 3218 bool $isDeprecated, 3219 bool $isStrictProperties, 3220 array $attributes, 3221 ?ExposedDocComment $exposedDocComment, 3222 bool $isNotSerializable, 3223 array $extends, 3224 array $implements, 3225 array $constInfos, 3226 array $propertyInfos, 3227 array $funcInfos, 3228 array $enumCaseInfos, 3229 ?string $cond, 3230 ?int $minimumPhpVersionIdCompatibility, 3231 bool $isUndocumentable 3232 ) { 3233 $this->name = $name; 3234 $this->flags = $flags; 3235 $this->type = $type; 3236 $this->alias = $alias; 3237 $this->enumBackingType = $enumBackingType; 3238 $this->isDeprecated = $isDeprecated; 3239 $this->isStrictProperties = $isStrictProperties; 3240 $this->attributes = $attributes; 3241 $this->exposedDocComment = $exposedDocComment; 3242 $this->isNotSerializable = $isNotSerializable; 3243 $this->extends = $extends; 3244 $this->implements = $implements; 3245 $this->constInfos = $constInfos; 3246 $this->propertyInfos = $propertyInfos; 3247 $this->funcInfos = $funcInfos; 3248 $this->enumCaseInfos = $enumCaseInfos; 3249 $this->cond = $cond; 3250 $this->phpVersionIdMinimumCompatibility = $minimumPhpVersionIdCompatibility; 3251 $this->isUndocumentable = $isUndocumentable; 3252 } 3253 3254 /** @param array<string, ConstInfo> $allConstInfos */ 3255 public function getRegistration(array $allConstInfos): string 3256 { 3257 $params = []; 3258 foreach ($this->extends as $extends) { 3259 $params[] = "zend_class_entry *class_entry_" . implode("_", $extends->getParts()); 3260 } 3261 foreach ($this->implements as $implements) { 3262 $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->getParts()); 3263 } 3264 3265 $escapedName = implode("_", $this->name->getParts()); 3266 3267 $code = ''; 3268 3269 $php80MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID; 3270 $php81MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_81_VERSION_ID; 3271 $php84MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_84_VERSION_ID; 3272 3273 if ($this->type === "enum" && !$php81MinimumCompatibility) { 3274 $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n"; 3275 } 3276 3277 if ($this->cond) { 3278 $code .= "#if {$this->cond}\n"; 3279 } 3280 3281 $code .= "static zend_class_entry *register_class_$escapedName(" . (empty($params) ? "void" : implode(", ", $params)) . ")\n"; 3282 3283 $code .= "{\n"; 3284 3285 $flagCodes = generateVersionDependentFlagCode("%s", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility); 3286 $flags = implode("", $flagCodes); 3287 3288 $classMethods = ($this->funcInfos === []) ? 'NULL' : "class_{$escapedName}_methods"; 3289 if ($this->type === "enum") { 3290 $name = addslashes((string) $this->name); 3291 $backingType = $this->enumBackingType 3292 ? $this->enumBackingType->toTypeCode() : "IS_UNDEF"; 3293 $code .= "\tzend_class_entry *class_entry = zend_register_internal_enum(\"$name\", $backingType, $classMethods);\n"; 3294 if ($flags !== "") { 3295 $code .= "\tclass_entry->ce_flags |= $flags\n"; 3296 } 3297 } else { 3298 $code .= "\tzend_class_entry ce, *class_entry;\n\n"; 3299 if (count($this->name->getParts()) > 1) { 3300 $className = $this->name->getLast(); 3301 $namespace = addslashes((string) $this->name->slice(0, -1)); 3302 3303 $code .= "\tINIT_NS_CLASS_ENTRY(ce, \"$namespace\", \"$className\", $classMethods);\n"; 3304 } else { 3305 $code .= "\tINIT_CLASS_ENTRY(ce, \"$this->name\", $classMethods);\n"; 3306 } 3307 3308 if ($this->type === "class" || $this->type === "trait") { 3309 if (!$php84MinimumCompatibility) { 3310 $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; 3311 } 3312 3313 $code .= "\tclass_entry = zend_register_internal_class_with_flags(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ", " . ($flags ?: 0) . ");\n"; 3314 3315 if (!$php84MinimumCompatibility) { 3316 $code .= "#else\n"; 3317 3318 $code .= "\tclass_entry = zend_register_internal_class_ex(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ");\n"; 3319 if ($flags !== "") { 3320 $code .= "\tclass_entry->ce_flags |= $flags;\n"; 3321 } 3322 $code .= "#endif\n"; 3323 } 3324 } else { 3325 $code .= "\tclass_entry = zend_register_internal_interface(&ce);\n"; 3326 if ($flags !== "") { 3327 $code .= "\tclass_entry->ce_flags |= $flags\n"; 3328 } 3329 } 3330 } 3331 3332 if ($this->exposedDocComment) { 3333 if (!$php84MinimumCompatibility) { 3334 $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n"; 3335 } 3336 3337 $code .= "\tclass_entry->doc_comment = zend_string_init_interned(\"" . $this->exposedDocComment->escape() . "\", " . $this->exposedDocComment->getLength() . ", 1);\n"; 3338 3339 if (!$php84MinimumCompatibility) { 3340 $code .= "#endif\n"; 3341 } 3342 } 3343 3344 $implements = array_map( 3345 function (Name $item) { 3346 return "class_entry_" . implode("_", $item->getParts()); 3347 }, 3348 $this->type === "interface" ? $this->extends : $this->implements 3349 ); 3350 3351 if (!empty($implements)) { 3352 $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n"; 3353 } 3354 3355 if ($this->alias) { 3356 $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n"; 3357 } 3358 3359 foreach ($this->constInfos as $const) { 3360 $code .= $const->getDeclaration($allConstInfos); 3361 } 3362 3363 foreach ($this->enumCaseInfos as $enumCase) { 3364 $code .= $enumCase->getDeclaration($allConstInfos); 3365 } 3366 3367 foreach ($this->propertyInfos as $property) { 3368 $code .= $property->getDeclaration($allConstInfos); 3369 } 3370 3371 if (!empty($this->attributes)) { 3372 if (!$php80MinimumCompatibility) { 3373 $code .= "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; 3374 } 3375 3376 foreach ($this->attributes as $key => $attribute) { 3377 $code .= $attribute->generateCode( 3378 "zend_add_class_attribute(class_entry", 3379 "class_{$escapedName}_$key", 3380 $allConstInfos, 3381 $this->phpVersionIdMinimumCompatibility 3382 ); 3383 } 3384 3385 if (!$php80MinimumCompatibility) { 3386 $code .= "#endif\n"; 3387 } 3388 } 3389 3390 if ($attributeInitializationCode = generateConstantAttributeInitialization($this->constInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) { 3391 if (!$php80MinimumCompatibility) { 3392 $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; 3393 } 3394 3395 $code .= "\n" . $attributeInitializationCode; 3396 3397 if (!$php80MinimumCompatibility) { 3398 $code .= "#endif\n"; 3399 } 3400 } 3401 3402 if ($attributeInitializationCode = generatePropertyAttributeInitialization($this->propertyInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility)) { 3403 if (!$php80MinimumCompatibility) { 3404 $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")"; 3405 } 3406 3407 $code .= "\n" . $attributeInitializationCode; 3408 3409 if (!$php80MinimumCompatibility) { 3410 $code .= "#endif\n"; 3411 } 3412 } 3413 3414 if ($attributeInitializationCode = generateFunctionAttributeInitialization($this->funcInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) { 3415 if (!$php80MinimumCompatibility) { 3416 $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")\n"; 3417 } 3418 3419 $code .= "\n" . $attributeInitializationCode; 3420 3421 if (!$php80MinimumCompatibility) { 3422 $code .= "#endif\n"; 3423 } 3424 } 3425 3426 $code .= "\n\treturn class_entry;\n"; 3427 3428 $code .= "}\n"; 3429 3430 if ($this->cond) { 3431 $code .= "#endif\n"; 3432 } 3433 3434 if ($this->type === "enum" && !$php81MinimumCompatibility) { 3435 $code .= "#endif\n"; 3436 } 3437 3438 return $code; 3439 } 3440 3441 /** 3442 * @return array<int, string[]> 3443 */ 3444 private function getFlagsByPhpVersion(): array 3445 { 3446 $php70Flags = []; 3447 3448 if ($this->type === "trait") { 3449 $php70Flags[] = "ZEND_ACC_TRAIT"; 3450 } 3451 3452 if ($this->flags & Modifiers::FINAL) { 3453 $php70Flags[] = "ZEND_ACC_FINAL"; 3454 } 3455 3456 if ($this->flags & Modifiers::ABSTRACT) { 3457 $php70Flags[] = "ZEND_ACC_ABSTRACT"; 3458 } 3459 3460 if ($this->isDeprecated) { 3461 $php70Flags[] = "ZEND_ACC_DEPRECATED"; 3462 } 3463 3464 $php80Flags = $php70Flags; 3465 3466 if ($this->isStrictProperties) { 3467 $php80Flags[] = "ZEND_ACC_NO_DYNAMIC_PROPERTIES"; 3468 } 3469 3470 $php81Flags = $php80Flags; 3471 3472 if ($this->isNotSerializable) { 3473 $php81Flags[] = "ZEND_ACC_NOT_SERIALIZABLE"; 3474 } 3475 3476 $php82Flags = $php81Flags; 3477 3478 if ($this->flags & Modifiers::READONLY) { 3479 $php82Flags[] = "ZEND_ACC_READONLY_CLASS"; 3480 } 3481 3482 foreach ($this->attributes as $attr) { 3483 if ($attr->class === "AllowDynamicProperties") { 3484 $php82Flags[] = "ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES"; 3485 break; 3486 } 3487 } 3488 3489 $php83Flags = $php82Flags; 3490 $php84Flags = $php83Flags; 3491 3492 return [ 3493 PHP_70_VERSION_ID => $php70Flags, 3494 PHP_80_VERSION_ID => $php80Flags, 3495 PHP_81_VERSION_ID => $php81Flags, 3496 PHP_82_VERSION_ID => $php82Flags, 3497 PHP_83_VERSION_ID => $php83Flags, 3498 PHP_84_VERSION_ID => $php84Flags, 3499 ]; 3500 } 3501 3502 public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void { 3503 $this->attributes = []; 3504 $this->flags &= ~Modifiers::READONLY; 3505 $this->exposedDocComment = null; 3506 $this->isStrictProperties = false; 3507 $this->isNotSerializable = false; 3508 3509 foreach ($this->propertyInfos as $propertyInfo) { 3510 $propertyInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility); 3511 } 3512 $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility; 3513 } 3514 3515 /** 3516 * @param array<string, ClassInfo> $classMap 3517 * @param array<string, ConstInfo> $allConstInfos 3518 * @param iterable<ConstInfo> $allConstInfo 3519 */ 3520 public function getClassSynopsisDocument(array $classMap, array $allConstInfos): ?string { 3521 3522 $doc = new DOMDocument(); 3523 $doc->formatOutput = true; 3524 $classSynopsis = $this->getClassSynopsisElement($doc, $classMap, $allConstInfos); 3525 if (!$classSynopsis) { 3526 return null; 3527 } 3528 3529 $doc->appendChild($classSynopsis); 3530 3531 return $doc->saveXML(); 3532 } 3533 3534 /** 3535 * @param array<string, ClassInfo> $classMap 3536 * @param array<string, ConstInfo> $allConstInfos 3537 */ 3538 public function getClassSynopsisElement(DOMDocument $doc, array $classMap, array $allConstInfos): ?DOMElement { 3539 3540 $classSynopsis = $doc->createElement("classsynopsis"); 3541 $classSynopsis->setAttribute("class", $this->type === "interface" ? "interface" : "class"); 3542 3543 $exceptionOverride = $this->type === "class" && $this->isException($classMap) ? "exception" : null; 3544 $ooElement = self::createOoElement($doc, $this, $exceptionOverride, true, null, 4); 3545 if (!$ooElement) { 3546 return null; 3547 } 3548 $classSynopsis->appendChild(new DOMText("\n ")); 3549 $classSynopsis->appendChild($ooElement); 3550 3551 foreach ($this->extends as $k => $parent) { 3552 $parentInfo = $classMap[$parent->toString()] ?? null; 3553 if ($parentInfo === null) { 3554 throw new Exception("Missing parent class " . $parent->toString()); 3555 } 3556 3557 $ooElement = self::createOoElement( 3558 $doc, 3559 $parentInfo, 3560 null, 3561 false, 3562 $k === 0 ? "extends" : null, 3563 4 3564 ); 3565 if (!$ooElement) { 3566 return null; 3567 } 3568 3569 $classSynopsis->appendChild(new DOMText("\n\n ")); 3570 $classSynopsis->appendChild($ooElement); 3571 } 3572 3573 foreach ($this->implements as $k => $interface) { 3574 $interfaceInfo = $classMap[$interface->toString()] ?? null; 3575 if (!$interfaceInfo) { 3576 throw new Exception("Missing implemented interface " . $interface->toString()); 3577 } 3578 3579 $ooElement = self::createOoElement($doc, $interfaceInfo, null, false, $k === 0 ? "implements" : null, 4); 3580 if (!$ooElement) { 3581 return null; 3582 } 3583 $classSynopsis->appendChild(new DOMText("\n\n ")); 3584 $classSynopsis->appendChild($ooElement); 3585 } 3586 3587 /** @var array<string, Name> $parentsWithInheritedConstants */ 3588 $parentsWithInheritedConstants = []; 3589 /** @var array<string, Name> $parentsWithInheritedProperties */ 3590 $parentsWithInheritedProperties = []; 3591 /** @var array<int, array{name: Name, types: int[]}> $parentsWithInheritedMethods */ 3592 $parentsWithInheritedMethods = []; 3593 3594 $this->collectInheritedMembers( 3595 $parentsWithInheritedConstants, 3596 $parentsWithInheritedProperties, 3597 $parentsWithInheritedMethods, 3598 $this->hasConstructor(), 3599 $classMap 3600 ); 3601 3602 $this->appendInheritedMemberSectionToClassSynopsis( 3603 $doc, 3604 $classSynopsis, 3605 $parentsWithInheritedConstants, 3606 "&Constants;", 3607 "&InheritedConstants;" 3608 ); 3609 3610 if (!empty($this->constInfos)) { 3611 $classSynopsis->appendChild(new DOMText("\n\n ")); 3612 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Constants;"); 3613 $classSynopsisInfo->setAttribute("role", "comment"); 3614 $classSynopsis->appendChild($classSynopsisInfo); 3615 3616 foreach ($this->constInfos as $constInfo) { 3617 $classSynopsis->appendChild(new DOMText("\n ")); 3618 $fieldSynopsisElement = $constInfo->getFieldSynopsisElement($doc, $allConstInfos); 3619 $classSynopsis->appendChild($fieldSynopsisElement); 3620 } 3621 } 3622 3623 if (!empty($this->propertyInfos)) { 3624 $classSynopsis->appendChild(new DOMText("\n\n ")); 3625 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;"); 3626 $classSynopsisInfo->setAttribute("role", "comment"); 3627 $classSynopsis->appendChild($classSynopsisInfo); 3628 3629 foreach ($this->propertyInfos as $propertyInfo) { 3630 $classSynopsis->appendChild(new DOMText("\n ")); 3631 $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc, $allConstInfos); 3632 $classSynopsis->appendChild($fieldSynopsisElement); 3633 } 3634 } 3635 3636 $this->appendInheritedMemberSectionToClassSynopsis( 3637 $doc, 3638 $classSynopsis, 3639 $parentsWithInheritedProperties, 3640 "&Properties;", 3641 "&InheritedProperties;" 3642 ); 3643 3644 if (!empty($this->funcInfos)) { 3645 $classSynopsis->appendChild(new DOMText("\n\n ")); 3646 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Methods;"); 3647 $classSynopsisInfo->setAttribute("role", "comment"); 3648 $classSynopsis->appendChild($classSynopsisInfo); 3649 3650 $classReference = self::getClassSynopsisReference($this->name); 3651 $escapedName = addslashes($this->name->__toString()); 3652 3653 if ($this->hasConstructor()) { 3654 $classSynopsis->appendChild(new DOMText("\n ")); 3655 $includeElement = $this->createIncludeElement( 3656 $doc, 3657 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[@role='$escapedName'])" 3658 ); 3659 $classSynopsis->appendChild($includeElement); 3660 } 3661 3662 if ($this->hasMethods()) { 3663 $classSynopsis->appendChild(new DOMText("\n ")); 3664 $includeElement = $this->createIncludeElement( 3665 $doc, 3666 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[@role='$escapedName'])" 3667 ); 3668 $classSynopsis->appendChild($includeElement); 3669 } 3670 3671 if ($this->hasDestructor()) { 3672 $classSynopsis->appendChild(new DOMText("\n ")); 3673 $includeElement = $this->createIncludeElement( 3674 $doc, 3675 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[@role='$escapedName'])" 3676 ); 3677 $classSynopsis->appendChild($includeElement); 3678 } 3679 } 3680 3681 if (!empty($parentsWithInheritedMethods)) { 3682 $classSynopsis->appendChild(new DOMText("\n\n ")); 3683 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&InheritedMethods;"); 3684 $classSynopsisInfo->setAttribute("role", "comment"); 3685 $classSynopsis->appendChild($classSynopsisInfo); 3686 3687 foreach ($parentsWithInheritedMethods as $parent) { 3688 $parentName = $parent["name"]; 3689 $parentMethodsynopsisTypes = $parent["types"]; 3690 3691 $parentReference = self::getClassSynopsisReference($parentName); 3692 $escapedParentName = addslashes($parentName->__toString()); 3693 3694 foreach ($parentMethodsynopsisTypes as $parentMethodsynopsisType) { 3695 $classSynopsis->appendChild(new DOMText("\n ")); 3696 $includeElement = $this->createIncludeElement( 3697 $doc, 3698 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$parentReference')/db:refentry/db:refsect1[@role='description']/descendant::db:{$parentMethodsynopsisType}[@role='$escapedParentName'])" 3699 ); 3700 3701 $classSynopsis->appendChild($includeElement); 3702 } 3703 } 3704 } 3705 3706 $classSynopsis->appendChild(new DOMText("\n ")); 3707 3708 return $classSynopsis; 3709 } 3710 3711 private static function createOoElement( 3712 DOMDocument $doc, 3713 ClassInfo $classInfo, 3714 ?string $typeOverride, 3715 bool $withModifiers, 3716 ?string $modifierOverride, 3717 int $indentationLevel 3718 ): ?DOMElement { 3719 $indentation = str_repeat(" ", $indentationLevel); 3720 3721 if ($classInfo->type !== "class" && $classInfo->type !== "interface") { 3722 echo "Class synopsis generation is not implemented for " . $classInfo->type . "\n"; 3723 return null; 3724 } 3725 3726 $type = $typeOverride !== null ? $typeOverride : $classInfo->type; 3727 3728 $ooElement = $doc->createElement("oo$type"); 3729 $ooElement->appendChild(new DOMText("\n$indentation ")); 3730 if ($modifierOverride !== null) { 3731 $ooElement->appendChild($doc->createElement('modifier', $modifierOverride)); 3732 $ooElement->appendChild(new DOMText("\n$indentation ")); 3733 } elseif ($withModifiers) { 3734 if ($classInfo->flags & Modifiers::FINAL) { 3735 $ooElement->appendChild($doc->createElement('modifier', 'final')); 3736 $ooElement->appendChild(new DOMText("\n$indentation ")); 3737 } 3738 if ($classInfo->flags & Modifiers::ABSTRACT) { 3739 $ooElement->appendChild($doc->createElement('modifier', 'abstract')); 3740 $ooElement->appendChild(new DOMText("\n$indentation ")); 3741 } 3742 if ($classInfo->flags & Modifiers::READONLY) { 3743 $ooElement->appendChild($doc->createElement('modifier', 'readonly')); 3744 $ooElement->appendChild(new DOMText("\n$indentation ")); 3745 } 3746 } 3747 3748 $nameElement = $doc->createElement("{$type}name", $classInfo->name->toString()); 3749 $ooElement->appendChild($nameElement); 3750 $ooElement->appendChild(new DOMText("\n$indentation")); 3751 3752 return $ooElement; 3753 } 3754 3755 public static function getClassSynopsisFilename(Name $name): string { 3756 return strtolower(str_replace("_", "-", implode('-', $name->getParts()))); 3757 } 3758 3759 public static function getClassSynopsisReference(Name $name): string { 3760 return "class." . self::getClassSynopsisFilename($name); 3761 } 3762 3763 /** 3764 * @param array<string, Name> $parentsWithInheritedConstants 3765 * @param array<string, Name> $parentsWithInheritedProperties 3766 * @param array<string, array{name: Name, types: int[]}> $parentsWithInheritedMethods 3767 * @param array<string, ClassInfo> $classMap 3768 */ 3769 private function collectInheritedMembers( 3770 array &$parentsWithInheritedConstants, 3771 array &$parentsWithInheritedProperties, 3772 array &$parentsWithInheritedMethods, 3773 bool $hasConstructor, 3774 array $classMap 3775 ): void { 3776 foreach ($this->extends as $parent) { 3777 $parentInfo = $classMap[$parent->toString()] ?? null; 3778 $parentName = $parent->toString(); 3779 3780 if (!$parentInfo) { 3781 throw new Exception("Missing parent class $parentName"); 3782 } 3783 3784 if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parentName])) { 3785 $parentsWithInheritedConstants[] = $parent; 3786 } 3787 3788 if (!empty($parentInfo->propertyInfos) && !isset($parentsWithInheritedProperties[$parentName])) { 3789 $parentsWithInheritedProperties[$parentName] = $parent; 3790 } 3791 3792 if (!$hasConstructor && $parentInfo->hasNonPrivateConstructor()) { 3793 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 3794 $parentsWithInheritedMethods[$parentName]["types"][] = "constructorsynopsis"; 3795 } 3796 3797 if ($parentInfo->hasMethods()) { 3798 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 3799 $parentsWithInheritedMethods[$parentName]["types"][] = "methodsynopsis"; 3800 } 3801 3802 if ($parentInfo->hasDestructor()) { 3803 $parentsWithInheritedMethods[$parentName]["name"] = $parent; 3804 $parentsWithInheritedMethods[$parentName]["types"][] = "destructorsynopsis"; 3805 } 3806 3807 $parentInfo->collectInheritedMembers( 3808 $parentsWithInheritedConstants, 3809 $parentsWithInheritedProperties, 3810 $parentsWithInheritedMethods, 3811 $hasConstructor, 3812 $classMap 3813 ); 3814 } 3815 3816 foreach ($this->implements as $parent) { 3817 $parentInfo = $classMap[$parent->toString()] ?? null; 3818 if (!$parentInfo) { 3819 throw new Exception("Missing parent interface " . $parent->toString()); 3820 } 3821 3822 if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parent->toString()])) { 3823 $parentsWithInheritedConstants[$parent->toString()] = $parent; 3824 } 3825 3826 $unusedParentsWithInheritedProperties = []; 3827 $unusedParentsWithInheritedMethods = []; 3828 3829 $parentInfo->collectInheritedMembers( 3830 $parentsWithInheritedConstants, 3831 $unusedParentsWithInheritedProperties, 3832 $unusedParentsWithInheritedMethods, 3833 $hasConstructor, 3834 $classMap 3835 ); 3836 } 3837 } 3838 3839 /** @param array<string, ClassInfo> $classMap */ 3840 private function isException(array $classMap): bool 3841 { 3842 if ($this->name->toString() === "Throwable") { 3843 return true; 3844 } 3845 3846 foreach ($this->extends as $parentName) { 3847 $parent = $classMap[$parentName->toString()] ?? null; 3848 if ($parent === null) { 3849 throw new Exception("Missing parent class " . $parentName->toString()); 3850 } 3851 3852 if ($parent->isException($classMap)) { 3853 return true; 3854 } 3855 } 3856 3857 if ($this->type === "class") { 3858 foreach ($this->implements as $interfaceName) { 3859 $interface = $classMap[$interfaceName->toString()] ?? null; 3860 if ($interface === null) { 3861 throw new Exception("Missing implemented interface " . $interfaceName->toString()); 3862 } 3863 3864 if ($interface->isException($classMap)) { 3865 return true; 3866 } 3867 } 3868 } 3869 3870 return false; 3871 } 3872 3873 private function hasConstructor(): bool 3874 { 3875 foreach ($this->funcInfos as $funcInfo) { 3876 if ($funcInfo->name->isConstructor()) { 3877 return true; 3878 } 3879 } 3880 3881 return false; 3882 } 3883 3884 private function hasNonPrivateConstructor(): bool 3885 { 3886 foreach ($this->funcInfos as $funcInfo) { 3887 if ($funcInfo->name->isConstructor() && !($funcInfo->flags & Modifiers::PRIVATE)) { 3888 return true; 3889 } 3890 } 3891 3892 return false; 3893 } 3894 3895 private function hasDestructor(): bool 3896 { 3897 foreach ($this->funcInfos as $funcInfo) { 3898 if ($funcInfo->name->isDestructor()) { 3899 return true; 3900 } 3901 } 3902 3903 return false; 3904 } 3905 3906 private function hasMethods(): bool 3907 { 3908 foreach ($this->funcInfos as $funcInfo) { 3909 if (!$funcInfo->name->isConstructor() && !$funcInfo->name->isDestructor()) { 3910 return true; 3911 } 3912 } 3913 3914 return false; 3915 } 3916 3917 private function createIncludeElement(DOMDocument $doc, string $query): DOMElement 3918 { 3919 $includeElement = $doc->createElement("xi:include"); 3920 $attr = $doc->createAttribute("xpointer"); 3921 $attr->value = $query; 3922 $includeElement->appendChild($attr); 3923 $fallbackElement = $doc->createElement("xi:fallback"); 3924 $includeElement->appendChild(new DOMText("\n ")); 3925 $includeElement->appendChild($fallbackElement); 3926 $includeElement->appendChild(new DOMText("\n ")); 3927 3928 return $includeElement; 3929 } 3930 3931 public function __clone() 3932 { 3933 foreach ($this->constInfos as $key => $constInfo) { 3934 $this->constInfos[$key] = clone $constInfo; 3935 } 3936 3937 foreach ($this->propertyInfos as $key => $propertyInfo) { 3938 $this->propertyInfos[$key] = clone $propertyInfo; 3939 } 3940 3941 foreach ($this->funcInfos as $key => $funcInfo) { 3942 $this->funcInfos[$key] = clone $funcInfo; 3943 } 3944 3945 foreach ($this->attributes as $key => $attribute) { 3946 $this->attributes[$key] = clone $attribute; 3947 } 3948 3949 if ($this->exposedDocComment) { 3950 $this->exposedDocComment = clone $this->exposedDocComment; 3951 } 3952 } 3953 3954 /** 3955 * @param Name[] $parents 3956 */ 3957 private function appendInheritedMemberSectionToClassSynopsis(DOMDocument $doc, DOMElement $classSynopsis, array $parents, string $label, string $inheritedLabel): void 3958 { 3959 if (empty($parents)) { 3960 return; 3961 } 3962 3963 $classSynopsis->appendChild(new DOMText("\n\n ")); 3964 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "$inheritedLabel"); 3965 $classSynopsisInfo->setAttribute("role", "comment"); 3966 $classSynopsis->appendChild($classSynopsisInfo); 3967 3968 foreach ($parents as $parent) { 3969 $classSynopsis->appendChild(new DOMText("\n ")); 3970 $parentReference = self::getClassSynopsisReference($parent); 3971 3972 $includeElement = $this->createIncludeElement( 3973 $doc, 3974 "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']]))" 3975 ); 3976 $classSynopsis->appendChild($includeElement); 3977 } 3978 } 3979} 3980 3981class FileInfo { 3982 /** @var string[] */ 3983 public array $dependencies = []; 3984 /** @var ConstInfo[] */ 3985 public array $constInfos = []; 3986 /** @var FuncInfo[] */ 3987 public array $funcInfos = []; 3988 /** @var ClassInfo[] */ 3989 public array $classInfos = []; 3990 public bool $generateFunctionEntries = false; 3991 public string $declarationPrefix = ""; 3992 public bool $generateClassEntries = false; 3993 public bool $isUndocumentable = false; 3994 public bool $legacyArginfoGeneration = false; 3995 private ?int $minimumPhpVersionIdCompatibility = null; 3996 3997 /** 3998 * @return iterable<FuncInfo> 3999 */ 4000 public function getAllFuncInfos(): iterable { 4001 yield from $this->funcInfos; 4002 foreach ($this->classInfos as $classInfo) { 4003 yield from $classInfo->funcInfos; 4004 } 4005 } 4006 4007 /** @return array<string, ConstInfo> */ 4008 public function getAllConstInfos(): array { 4009 $result = []; 4010 4011 foreach ($this->constInfos as $constInfo) { 4012 $result[$constInfo->name->__toString()] = $constInfo; 4013 } 4014 4015 foreach ($this->classInfos as $classInfo) { 4016 foreach ($classInfo->constInfos as $constInfo) { 4017 $result[$constInfo->name->__toString()] = $constInfo; 4018 } 4019 } 4020 4021 return $result; 4022 } 4023 4024 /** 4025 * @return iterable<ClassInfo> 4026 */ 4027 public function getAllClassInfos(): iterable { 4028 foreach ($this->classInfos as $classInfo) { 4029 yield $classInfo; 4030 } 4031 } 4032 4033 public function __clone() 4034 { 4035 foreach ($this->constInfos as $key => $constInfo) { 4036 $this->constInfos[$key] = clone $constInfo; 4037 } 4038 4039 foreach ($this->funcInfos as $key => $funcInfo) { 4040 $this->funcInfos[$key] = clone $funcInfo; 4041 } 4042 4043 foreach ($this->classInfos as $key => $classInfo) { 4044 $this->classInfos[$key] = clone $classInfo; 4045 } 4046 } 4047 4048 public function setMinimumPhpVersionIdCompatibility(?int $minimumPhpVersionIdCompatibility) { 4049 $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility; 4050 } 4051 4052 public function getMinimumPhpVersionIdCompatibility(): ?int { 4053 // Non-legacy arginfo files are always PHP 8.0+ compatible 4054 if (!$this->legacyArginfoGeneration && 4055 $this->minimumPhpVersionIdCompatibility !== null && 4056 $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID 4057 ) { 4058 return PHP_80_VERSION_ID; 4059 } 4060 4061 return $this->minimumPhpVersionIdCompatibility; 4062 } 4063 4064 public function shouldGenerateLegacyArginfo(): bool { 4065 return $this->minimumPhpVersionIdCompatibility !== null && $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID; 4066 } 4067} 4068 4069class DocCommentTag { 4070 public string $name; 4071 public ?string $value; 4072 4073 public function __construct(string $name, ?string $value) { 4074 $this->name = $name; 4075 $this->value = $value; 4076 } 4077 4078 public function getValue(): string { 4079 if ($this->value === null) { 4080 throw new Exception("@$this->name does not have a value"); 4081 } 4082 4083 return $this->value; 4084 } 4085 4086 public function getType(): string { 4087 $value = $this->getValue(); 4088 4089 $matches = []; 4090 4091 if ($this->name === "param") { 4092 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)\s*(?:[{(]|\$\w+).*$/', $value, $matches); 4093 } elseif ($this->name === "return" || $this->name === "var") { 4094 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)/', $value, $matches); 4095 } 4096 4097 if (!isset($matches[1])) { 4098 throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\""); 4099 } 4100 4101 return trim($matches[1]); 4102 } 4103 4104 public function getVariableName(): string { 4105 $value = $this->value; 4106 if ($value === null || strlen($value) === 0) { 4107 throw new Exception("@$this->name doesn't have any value"); 4108 } 4109 4110 $matches = []; 4111 4112 if ($this->name === "param") { 4113 // Allow for parsing extended types like callable(string):mixed in docblocks 4114 preg_match('/^\s*(?<type>[\w\|\\\\]+(?<parens>\((?<inparens>(?:(?&parens)|[^(){}[\]]*+))++\)|\{(?&inparens)\}|\[(?&inparens)\])*+(?::(?&type))?)\s*\$(?<name>\w+).*$/', $value, $matches); 4115 } elseif ($this->name === "prefer-ref") { 4116 preg_match('/^\s*\$(?<name>\w+).*$/', $value, $matches); 4117 } 4118 4119 if (!isset($matches["name"])) { 4120 throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\""); 4121 } 4122 4123 return $matches["name"]; 4124 } 4125} 4126 4127class ExposedDocComment { 4128 private string $docComment; 4129 4130 public function __construct(string $docComment) { 4131 $this->docComment = $docComment; 4132 } 4133 4134 public function escape(): string { 4135 return str_replace("\n", '\n', addslashes($this->docComment)); 4136 } 4137 4138 public function getLength(): int { 4139 return strlen($this->docComment); 4140 } 4141} 4142 4143/** @return DocCommentTag[] */ 4144function parseDocComments(array $comments): array { 4145 $tags = []; 4146 foreach ($comments as $comment) { 4147 if ($comment instanceof DocComment) { 4148 $tags = array_merge($tags, parseDocComment($comment)); 4149 } 4150 } 4151 4152 return $tags; 4153} 4154 4155/** @return DocCommentTag[] */ 4156function parseDocComment(DocComment $comment): array { 4157 $commentText = substr($comment->getText(), 2, -2); 4158 $tags = []; 4159 foreach (explode("\n", $commentText) as $commentLine) { 4160 $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/'; 4161 if (preg_match($regex, trim($commentLine), $matches)) { 4162 $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null); 4163 } 4164 } 4165 4166 return $tags; 4167} 4168 4169class FramelessFunctionInfo { 4170 public int $arity; 4171} 4172 4173function parseFramelessFunctionInfo(string $json): FramelessFunctionInfo { 4174 // FIXME: Should have some validation 4175 $json = json_decode($json, true); 4176 $framelessFunctionInfo = new FramelessFunctionInfo(); 4177 $framelessFunctionInfo->arity = $json["arity"]; 4178 return $framelessFunctionInfo; 4179} 4180 4181function parseFunctionLike( 4182 PrettyPrinterAbstract $prettyPrinter, 4183 FunctionOrMethodName $name, 4184 int $classFlags, 4185 int $flags, 4186 Node\FunctionLike $func, 4187 ?string $cond, 4188 bool $isUndocumentable, 4189 ?int $minimumPhpVersionIdCompatibility 4190): FuncInfo { 4191 try { 4192 $comments = $func->getComments(); 4193 $paramMeta = []; 4194 $aliasType = null; 4195 $alias = null; 4196 $isDeprecated = false; 4197 $supportsCompileTimeEval = false; 4198 $verify = true; 4199 $docReturnType = null; 4200 $tentativeReturnType = false; 4201 $docParamTypes = []; 4202 $refcount = null; 4203 $framelessFunctionInfos = []; 4204 4205 if ($comments) { 4206 $tags = parseDocComments($comments); 4207 4208 foreach ($tags as $tag) { 4209 switch ($tag->name) { 4210 case 'alias': 4211 case 'implementation-alias': 4212 $aliasType = $tag->name; 4213 $aliasParts = explode("::", $tag->getValue()); 4214 if (count($aliasParts) === 1) { 4215 $alias = new FunctionName(new Name($aliasParts[0])); 4216 } else { 4217 $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]); 4218 } 4219 break; 4220 4221 case 'deprecated': 4222 $isDeprecated = true; 4223 break; 4224 4225 case 'no-verify': 4226 $verify = false; 4227 break; 4228 4229 case 'tentative-return-type': 4230 $tentativeReturnType = true; 4231 break; 4232 4233 case 'return': 4234 $docReturnType = $tag->getType(); 4235 break; 4236 4237 case 'param': 4238 $docParamTypes[$tag->getVariableName()] = $tag->getType(); 4239 break; 4240 4241 case 'refcount': 4242 $refcount = $tag->getValue(); 4243 break; 4244 4245 case 'compile-time-eval': 4246 $supportsCompileTimeEval = true; 4247 break; 4248 4249 case 'prefer-ref': 4250 $varName = $tag->getVariableName(); 4251 if (!isset($paramMeta[$varName])) { 4252 $paramMeta[$varName] = []; 4253 } 4254 $paramMeta[$varName][$tag->name] = true; 4255 break; 4256 4257 case 'undocumentable': 4258 $isUndocumentable = true; 4259 break; 4260 4261 case 'frameless-function': 4262 $framelessFunctionInfos[] = parseFramelessFunctionInfo($tag->getValue()); 4263 break; 4264 } 4265 } 4266 } 4267 4268 $varNameSet = []; 4269 $args = []; 4270 $numRequiredArgs = 0; 4271 $foundVariadic = false; 4272 foreach ($func->getParams() as $i => $param) { 4273 if ($param->isPromoted()) { 4274 throw new Exception("Promoted properties are not supported"); 4275 } 4276 4277 $varName = $param->var->name; 4278 $preferRef = !empty($paramMeta[$varName]['prefer-ref']); 4279 unset($paramMeta[$varName]); 4280 4281 if (isset($varNameSet[$varName])) { 4282 throw new Exception("Duplicate parameter name $varName"); 4283 } 4284 $varNameSet[$varName] = true; 4285 4286 if ($preferRef) { 4287 $sendBy = ArgInfo::SEND_PREFER_REF; 4288 } else if ($param->byRef) { 4289 $sendBy = ArgInfo::SEND_BY_REF; 4290 } else { 4291 $sendBy = ArgInfo::SEND_BY_VAL; 4292 } 4293 4294 if ($foundVariadic) { 4295 throw new Exception("Only the last parameter can be variadic"); 4296 } 4297 4298 $type = $param->type ? Type::fromNode($param->type) : null; 4299 if ($type === null && !isset($docParamTypes[$varName])) { 4300 throw new Exception("Missing parameter type"); 4301 } 4302 4303 if ($param->default instanceof Expr\ConstFetch && 4304 $param->default->name->toLowerString() === "null" && 4305 $type && !$type->isNullable() 4306 ) { 4307 $simpleType = $type->tryToSimpleType(); 4308 if ($simpleType === null || !$simpleType->isMixed()) { 4309 throw new Exception("Parameter $varName has null default, but is not nullable"); 4310 } 4311 } 4312 4313 if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { 4314 throw new Exception('The exact class name must be used instead of "self"'); 4315 } 4316 4317 $foundVariadic = $param->variadic; 4318 4319 $args[] = new ArgInfo( 4320 $varName, 4321 $sendBy, 4322 $param->variadic, 4323 $type, 4324 isset($docParamTypes[$varName]) ? Type::fromString($docParamTypes[$varName]) : null, 4325 $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null, 4326 createAttributes($param->attrGroups) 4327 ); 4328 if (!$param->default && !$param->variadic) { 4329 $numRequiredArgs = $i + 1; 4330 } 4331 } 4332 4333 foreach (array_keys($paramMeta) as $var) { 4334 throw new Exception("Found metadata for invalid param $var"); 4335 } 4336 4337 $returnType = $func->getReturnType(); 4338 if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { 4339 throw new Exception("Missing return type"); 4340 } 4341 4342 $return = new ReturnInfo( 4343 $func->returnsByRef(), 4344 $returnType ? Type::fromNode($returnType) : null, 4345 $docReturnType ? Type::fromString($docReturnType) : null, 4346 $tentativeReturnType, 4347 $refcount 4348 ); 4349 4350 return new FuncInfo( 4351 $name, 4352 $classFlags, 4353 $flags, 4354 $aliasType, 4355 $alias, 4356 $isDeprecated, 4357 $supportsCompileTimeEval, 4358 $verify, 4359 $args, 4360 $return, 4361 $numRequiredArgs, 4362 $cond, 4363 $isUndocumentable, 4364 $minimumPhpVersionIdCompatibility, 4365 createAttributes($func->attrGroups), 4366 $framelessFunctionInfos, 4367 createExposedDocComment($comments) 4368 ); 4369 } catch (Exception $e) { 4370 throw new Exception($name . "(): " .$e->getMessage()); 4371 } 4372} 4373 4374/** 4375 * @param array<int, array<int, AttributeGroup> $attributes 4376 */ 4377function parseConstLike( 4378 PrettyPrinterAbstract $prettyPrinter, 4379 ConstOrClassConstName $name, 4380 Node\Const_ $const, 4381 int $flags, 4382 ?Node $type, 4383 array $comments, 4384 ?string $cond, 4385 bool $isUndocumentable, 4386 ?int $phpVersionIdMinimumCompatibility, 4387 array $attributes 4388): ConstInfo { 4389 $phpDocType = null; 4390 $deprecated = false; 4391 $cValue = null; 4392 $link = null; 4393 $isFileCacheAllowed = true; 4394 if ($comments) { 4395 $tags = parseDocComments($comments); 4396 foreach ($tags as $tag) { 4397 if ($tag->name === 'var') { 4398 $phpDocType = $tag->getType(); 4399 } elseif ($tag->name === 'deprecated') { 4400 $deprecated = true; 4401 } elseif ($tag->name === 'cvalue') { 4402 $cValue = $tag->value; 4403 } elseif ($tag->name === 'undocumentable') { 4404 $isUndocumentable = true; 4405 } elseif ($tag->name === 'link') { 4406 $link = $tag->value; 4407 } elseif ($tag->name === 'no-file-cache') { 4408 $isFileCacheAllowed = false; 4409 } 4410 } 4411 } 4412 4413 if ($type === null && $phpDocType === null) { 4414 throw new Exception("Missing type for constant " . $name->__toString()); 4415 } 4416 4417 $constType = $type ? Type::fromNode($type) : null; 4418 $constPhpDocType = $phpDocType ? Type::fromString($phpDocType) : null; 4419 4420 if ($const->value instanceof Expr\ConstFetch && 4421 $const->value->name->toLowerString() === "null" && 4422 $constType && !$constType->isNullable() 4423 ) { 4424 $simpleType = $constType->tryToSimpleType(); 4425 if ($simpleType === null || !$simpleType->isMixed()) { 4426 throw new Exception("Constant " . $name->__toString() . " has null value, but is not nullable"); 4427 } 4428 } 4429 4430 return new ConstInfo( 4431 $name, 4432 $flags, 4433 $const->value, 4434 $prettyPrinter->prettyPrintExpr($const->value), 4435 $constType, 4436 $constPhpDocType, 4437 $deprecated, 4438 $cond, 4439 $cValue, 4440 $isUndocumentable, 4441 $link, 4442 $phpVersionIdMinimumCompatibility, 4443 $attributes, 4444 createExposedDocComment($comments), 4445 $isFileCacheAllowed 4446 ); 4447} 4448 4449/** 4450 * @param array<int, array<int, AttributeGroup> $attributes 4451 */ 4452function parseProperty( 4453 Name $class, 4454 int $classFlags, 4455 int $flags, 4456 Stmt\PropertyProperty $property, 4457 ?Node $type, 4458 array $comments, 4459 PrettyPrinterAbstract $prettyPrinter, 4460 ?int $phpVersionIdMinimumCompatibility, 4461 array $attributes 4462): PropertyInfo { 4463 $phpDocType = null; 4464 $isDocReadonly = false; 4465 $isVirtual = false; 4466 $link = null; 4467 4468 if ($comments) { 4469 $tags = parseDocComments($comments); 4470 foreach ($tags as $tag) { 4471 if ($tag->name === 'var') { 4472 $phpDocType = $tag->getType(); 4473 } elseif ($tag->name === 'readonly') { 4474 $isDocReadonly = true; 4475 } elseif ($tag->name === 'link') { 4476 $link = $tag->value; 4477 } elseif ($tag->name === 'virtual') { 4478 $isVirtual = true; 4479 } 4480 } 4481 } 4482 4483 $propertyType = $type ? Type::fromNode($type) : null; 4484 if ($propertyType === null && !$phpDocType) { 4485 throw new Exception("Missing type for property $class::\$$property->name"); 4486 } 4487 4488 if ($property->default instanceof Expr\ConstFetch && 4489 $property->default->name->toLowerString() === "null" && 4490 $propertyType && !$propertyType->isNullable() 4491 ) { 4492 $simpleType = $propertyType->tryToSimpleType(); 4493 if ($simpleType === null || !$simpleType->isMixed()) { 4494 throw new Exception("Property $class::\$$property->name has null default, but is not nullable"); 4495 } 4496 } 4497 4498 return new PropertyInfo( 4499 new PropertyName($class, $property->name->__toString()), 4500 $classFlags, 4501 $flags, 4502 $propertyType, 4503 $phpDocType ? Type::fromString($phpDocType) : null, 4504 $property->default, 4505 $property->default ? $prettyPrinter->prettyPrintExpr($property->default) : null, 4506 $isDocReadonly, 4507 $isVirtual, 4508 $link, 4509 $phpVersionIdMinimumCompatibility, 4510 $attributes, 4511 createExposedDocComment($comments) 4512 ); 4513} 4514 4515/** 4516 * @param ConstInfo[] $consts 4517 * @param PropertyInfo[] $properties 4518 * @param FuncInfo[] $methods 4519 * @param EnumCaseInfo[] $enumCases 4520 */ 4521function parseClass( 4522 Name $name, 4523 Stmt\ClassLike $class, 4524 array $consts, 4525 array $properties, 4526 array $methods, 4527 array $enumCases, 4528 ?string $cond, 4529 ?int $minimumPhpVersionIdCompatibility, 4530 bool $isUndocumentable 4531): ClassInfo { 4532 $flags = $class instanceof Class_ ? $class->flags : 0; 4533 $comments = $class->getComments(); 4534 $alias = null; 4535 $isDeprecated = false; 4536 $isStrictProperties = false; 4537 $isNotSerializable = false; 4538 $allowsDynamicProperties = false; 4539 $attributes = []; 4540 4541 if ($comments) { 4542 $tags = parseDocComments($comments); 4543 foreach ($tags as $tag) { 4544 if ($tag->name === 'alias') { 4545 $alias = $tag->getValue(); 4546 } else if ($tag->name === 'deprecated') { 4547 $isDeprecated = true; 4548 } else if ($tag->name === 'strict-properties') { 4549 $isStrictProperties = true; 4550 } else if ($tag->name === 'not-serializable') { 4551 $isNotSerializable = true; 4552 } else if ($tag->name === 'undocumentable') { 4553 $isUndocumentable = true; 4554 } 4555 } 4556 } 4557 4558 $attributes = createAttributes($class->attrGroups); 4559 foreach ($attributes as $attribute) { 4560 switch ($attribute->class) { 4561 case 'AllowDynamicProperties': 4562 $allowsDynamicProperties = true; 4563 break 2; 4564 } 4565 } 4566 4567 if ($isStrictProperties && $allowsDynamicProperties) { 4568 throw new Exception("A class may not have '@strict-properties' and '#[\\AllowDynamicProperties]' at the same time."); 4569 } 4570 4571 $extends = []; 4572 $implements = []; 4573 4574 if ($class instanceof Class_) { 4575 $classKind = "class"; 4576 if ($class->extends) { 4577 $extends[] = $class->extends; 4578 } 4579 $implements = $class->implements; 4580 } elseif ($class instanceof Interface_) { 4581 $classKind = "interface"; 4582 $extends = $class->extends; 4583 } else if ($class instanceof Trait_) { 4584 $classKind = "trait"; 4585 } else if ($class instanceof Enum_) { 4586 $classKind = "enum"; 4587 $implements = $class->implements; 4588 } else { 4589 throw new Exception("Unknown class kind " . get_class($class)); 4590 } 4591 4592 if ($isUndocumentable) { 4593 foreach ($methods as $method) { 4594 $method->isUndocumentable = true; 4595 } 4596 } 4597 4598 return new ClassInfo( 4599 $name, 4600 $flags, 4601 $classKind, 4602 $alias, 4603 $class instanceof Enum_ && $class->scalarType !== null 4604 ? SimpleType::fromNode($class->scalarType) : null, 4605 $isDeprecated, 4606 $isStrictProperties, 4607 $attributes, 4608 createExposedDocComment($comments), 4609 $isNotSerializable, 4610 $extends, 4611 $implements, 4612 $consts, 4613 $properties, 4614 $methods, 4615 $enumCases, 4616 $cond, 4617 $minimumPhpVersionIdCompatibility, 4618 $isUndocumentable 4619 ); 4620} 4621 4622/** 4623 * @param array<int, array<int, AttributeGroup>> $attributeGroups 4624 * @return Attribute[] 4625 */ 4626function createAttributes(array $attributeGroups): array { 4627 $attributes = []; 4628 4629 foreach ($attributeGroups as $attrGroup) { 4630 foreach ($attrGroup->attrs as $attr) { 4631 $attributes[] = new AttributeInfo($attr->name->toString(), $attr->args); 4632 } 4633 } 4634 4635 return $attributes; 4636} 4637 4638/** @param array<int, DocComment> $comments */ 4639function createExposedDocComment(array $comments): ?ExposedDocComment { 4640 $exposedDocComment = null; 4641 4642 foreach ($comments as $comment) { 4643 $text = $comment->getText(); 4644 $matches = []; 4645 $pattern = "#^(\s*\/\*\*)(\s*@genstubs-expose-comment-block)(\s*)$#m"; 4646 4647 if (preg_match($pattern, $text, $matches) !== 1) { 4648 continue; 4649 } 4650 4651 if ($exposedDocComment !== null) { 4652 throw new Exception("Only one PHPDoc comment block can be exposed"); 4653 } 4654 4655 $exposedDocComment = preg_replace($pattern, '$1$3', $text); 4656 } 4657 4658 return $exposedDocComment ? new ExposedDocComment($exposedDocComment) : null; 4659} 4660 4661function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string { 4662 foreach ($stmt->getComments() as $comment) { 4663 $text = trim($comment->getText()); 4664 if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) { 4665 $conds[] = $matches[1]; 4666 } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) { 4667 $conds[] = "defined($matches[1])"; 4668 } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) { 4669 $conds[] = "!defined($matches[1])"; 4670 } else if (preg_match('/^#\s*else$/', $text)) { 4671 if (empty($conds)) { 4672 throw new Exception("Encountered else without corresponding #if"); 4673 } 4674 $cond = array_pop($conds); 4675 $conds[] = "!($cond)"; 4676 } else if (preg_match('/^#\s*endif$/', $text)) { 4677 if (empty($conds)) { 4678 throw new Exception("Encountered #endif without corresponding #if"); 4679 } 4680 array_pop($conds); 4681 } else if ($text[0] === '#') { 4682 throw new Exception("Unrecognized preprocessor directive \"$text\""); 4683 } 4684 } 4685 4686 return empty($conds) ? null : implode(' && ', $conds); 4687} 4688 4689/** @return DocComment[] */ 4690function getFileDocComments(array $stmts): array { 4691 if (empty($stmts)) { 4692 return []; 4693 } 4694 4695 $comments = $stmts[0]->getComments(); 4696 4697 $result = []; 4698 foreach ($comments as $comment) { 4699 if ($comment instanceof DocComment) { 4700 $result[] = $comment; 4701 } 4702 } 4703 4704 return $result; 4705} 4706 4707function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) { 4708 $conds = []; 4709 foreach ($stmts as $stmt) { 4710 $cond = handlePreprocessorConditions($conds, $stmt); 4711 4712 if ($stmt instanceof Stmt\Nop) { 4713 continue; 4714 } 4715 4716 if ($stmt instanceof Stmt\Namespace_) { 4717 handleStatements($fileInfo, $stmt->stmts, $prettyPrinter); 4718 continue; 4719 } 4720 4721 if ($stmt instanceof Stmt\Const_) { 4722 foreach ($stmt->consts as $const) { 4723 $fileInfo->constInfos[] = parseConstLike( 4724 $prettyPrinter, 4725 new ConstName($const->namespacedName, $const->name->toString()), 4726 $const, 4727 0, 4728 null, 4729 $stmt->getComments(), 4730 $cond, 4731 $fileInfo->isUndocumentable, 4732 $fileInfo->getMinimumPhpVersionIdCompatibility(), 4733 [] 4734 ); 4735 } 4736 continue; 4737 } 4738 4739 if ($stmt instanceof Stmt\Function_) { 4740 $fileInfo->funcInfos[] = parseFunctionLike( 4741 $prettyPrinter, 4742 new FunctionName($stmt->namespacedName), 4743 0, 4744 0, 4745 $stmt, 4746 $cond, 4747 $fileInfo->isUndocumentable, 4748 $fileInfo->getMinimumPhpVersionIdCompatibility() 4749 ); 4750 continue; 4751 } 4752 4753 if ($stmt instanceof Stmt\ClassLike) { 4754 $className = $stmt->namespacedName; 4755 $constInfos = []; 4756 $propertyInfos = []; 4757 $methodInfos = []; 4758 $enumCaseInfos = []; 4759 foreach ($stmt->stmts as $classStmt) { 4760 $cond = handlePreprocessorConditions($conds, $classStmt); 4761 if ($classStmt instanceof Stmt\Nop) { 4762 continue; 4763 } 4764 4765 $classFlags = $stmt instanceof Class_ ? $stmt->flags : 0; 4766 $abstractFlag = $stmt instanceof Stmt\Interface_ ? Modifiers::ABSTRACT : 0; 4767 4768 if ($classStmt instanceof Stmt\ClassConst) { 4769 foreach ($classStmt->consts as $const) { 4770 $constInfos[] = parseConstLike( 4771 $prettyPrinter, 4772 new ClassConstName($className, $const->name->toString()), 4773 $const, 4774 $classStmt->flags, 4775 $classStmt->type, 4776 $classStmt->getComments(), 4777 $cond, 4778 $fileInfo->isUndocumentable, 4779 $fileInfo->getMinimumPhpVersionIdCompatibility(), 4780 createAttributes($classStmt->attrGroups) 4781 ); 4782 } 4783 } else if ($classStmt instanceof Stmt\Property) { 4784 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 4785 throw new Exception("Visibility modifier is required"); 4786 } 4787 foreach ($classStmt->props as $property) { 4788 $propertyInfos[] = parseProperty( 4789 $className, 4790 $classFlags, 4791 $classStmt->flags, 4792 $property, 4793 $classStmt->type, 4794 $classStmt->getComments(), 4795 $prettyPrinter, 4796 $fileInfo->getMinimumPhpVersionIdCompatibility(), 4797 createAttributes($classStmt->attrGroups) 4798 ); 4799 } 4800 } else if ($classStmt instanceof Stmt\ClassMethod) { 4801 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 4802 throw new Exception("Visibility modifier is required"); 4803 } 4804 $methodInfos[] = parseFunctionLike( 4805 $prettyPrinter, 4806 new MethodName($className, $classStmt->name->toString()), 4807 $classFlags, 4808 $classStmt->flags | $abstractFlag, 4809 $classStmt, 4810 $cond, 4811 $fileInfo->isUndocumentable, 4812 $fileInfo->getMinimumPhpVersionIdCompatibility() 4813 ); 4814 } else if ($classStmt instanceof Stmt\EnumCase) { 4815 $enumCaseInfos[] = new EnumCaseInfo( 4816 $classStmt->name->toString(), $classStmt->expr); 4817 } else { 4818 throw new Exception("Not implemented {$classStmt->getType()}"); 4819 } 4820 } 4821 4822 $fileInfo->classInfos[] = parseClass( 4823 $className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->getMinimumPhpVersionIdCompatibility(), $fileInfo->isUndocumentable 4824 ); 4825 continue; 4826 } 4827 4828 if ($stmt instanceof Stmt\Expression) { 4829 $expr = $stmt->expr; 4830 if ($expr instanceof Expr\Include_) { 4831 $fileInfo->dependencies[] = (string)EvaluatedValue::createFromExpression($expr->expr, null, null, [])->value; 4832 continue; 4833 } 4834 } 4835 4836 throw new Exception("Unexpected node {$stmt->getType()}"); 4837 } 4838 if (!empty($conds)) { 4839 throw new Exception("Unterminated preprocessor conditions"); 4840 } 4841} 4842 4843function parseStubFile(string $code): FileInfo { 4844 $lexer = new PhpParser\Lexer\Emulative(); 4845 $parser = new PhpParser\Parser\Php7($lexer); 4846 $nodeTraverser = new PhpParser\NodeTraverser; 4847 $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); 4848 $prettyPrinter = new class extends Standard { 4849 protected function pName_FullyQualified(Name\FullyQualified $node): string { 4850 return implode('\\', $node->getParts()); 4851 } 4852 }; 4853 4854 $stmts = $parser->parse($code); 4855 $nodeTraverser->traverse($stmts); 4856 4857 $fileInfo = new FileInfo; 4858 $fileDocComments = getFileDocComments($stmts); 4859 if ($fileDocComments !== []) { 4860 $fileTags = parseDocComments($fileDocComments); 4861 foreach ($fileTags as $tag) { 4862 if ($tag->name === 'generate-function-entries') { 4863 $fileInfo->generateFunctionEntries = true; 4864 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 4865 } else if ($tag->name === 'generate-legacy-arginfo') { 4866 if ($tag->value && !in_array((int) $tag->value, ALL_PHP_VERSION_IDS, true)) { 4867 throw new Exception( 4868 "Legacy PHP version must be one of: \"" . PHP_70_VERSION_ID . "\" (PHP 7.0), \"" . PHP_80_VERSION_ID . "\" (PHP 8.0), " . 4869 "\"" . PHP_81_VERSION_ID . "\" (PHP 8.1), \"" . PHP_82_VERSION_ID . "\" (PHP 8.2), \"" . PHP_83_VERSION_ID . "\" (PHP 8.3), " . 4870 "\"" . PHP_84_VERSION_ID . "\" (PHP 8.4), \"" . $tag->value . "\" provided" 4871 ); 4872 } 4873 4874 $fileInfo->setMinimumPhpVersionIdCompatibility($tag->value ? (int) $tag->value : PHP_70_VERSION_ID); 4875 } else if ($tag->name === 'generate-class-entries') { 4876 $fileInfo->generateClassEntries = true; 4877 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 4878 } else if ($tag->name === 'undocumentable') { 4879 $fileInfo->isUndocumentable = true; 4880 } 4881 } 4882 } 4883 4884 // Generating class entries require generating function/method entries 4885 if ($fileInfo->generateClassEntries && !$fileInfo->generateFunctionEntries) { 4886 $fileInfo->generateFunctionEntries = true; 4887 } 4888 4889 handleStatements($fileInfo, $stmts, $prettyPrinter); 4890 return $fileInfo; 4891} 4892 4893function funcInfoToCode(FileInfo $fileInfo, FuncInfo $funcInfo): string { 4894 $code = ''; 4895 $returnType = $funcInfo->return->type; 4896 $isTentativeReturnType = $funcInfo->return->tentativeReturnType; 4897 $php81MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_81_VERSION_ID; 4898 4899 if ($returnType !== null) { 4900 if ($isTentativeReturnType && !$php81MinimumCompatibility) { 4901 $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n"; 4902 } 4903 if (null !== $simpleReturnType = $returnType->tryToSimpleType()) { 4904 if ($simpleReturnType->isBuiltin) { 4905 $code .= sprintf( 4906 "%s(%s, %d, %d, %s, %d)\n", 4907 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX", 4908 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 4909 $funcInfo->numRequiredArgs, 4910 $simpleReturnType->toTypeCode(), $returnType->isNullable() 4911 ); 4912 } else { 4913 $code .= sprintf( 4914 "%s(%s, %d, %d, %s, %d)\n", 4915 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX", 4916 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 4917 $funcInfo->numRequiredArgs, 4918 $simpleReturnType->toEscapedName(), $returnType->isNullable() 4919 ); 4920 } 4921 } else { 4922 $arginfoType = $returnType->toArginfoType(); 4923 if ($arginfoType->hasClassType()) { 4924 $code .= sprintf( 4925 "%s(%s, %d, %d, %s, %s)\n", 4926 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX", 4927 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 4928 $funcInfo->numRequiredArgs, 4929 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask() 4930 ); 4931 } else { 4932 $code .= sprintf( 4933 "%s(%s, %d, %d, %s)\n", 4934 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX", 4935 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 4936 $funcInfo->numRequiredArgs, 4937 $arginfoType->toTypeMask() 4938 ); 4939 } 4940 } 4941 if ($isTentativeReturnType && !$php81MinimumCompatibility) { 4942 $code .= sprintf( 4943 "#else\nZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n#endif\n", 4944 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 4945 ); 4946 } 4947 } else { 4948 $code .= sprintf( 4949 "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", 4950 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 4951 ); 4952 } 4953 4954 foreach ($funcInfo->args as $argInfo) { 4955 $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG"; 4956 $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; 4957 $argType = $argInfo->type; 4958 if ($argType !== null) { 4959 if (null !== $simpleArgType = $argType->tryToSimpleType()) { 4960 if ($simpleArgType->isBuiltin) { 4961 $code .= sprintf( 4962 "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n", 4963 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 4964 $simpleArgType->toTypeCode(), $argType->isNullable(), 4965 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 4966 ); 4967 } else { 4968 $code .= sprintf( 4969 "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n", 4970 $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 4971 $simpleArgType->toEscapedName(), $argType->isNullable(), 4972 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 4973 ); 4974 } 4975 } else { 4976 $arginfoType = $argType->toArginfoType(); 4977 if ($arginfoType->hasClassType()) { 4978 $code .= sprintf( 4979 "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s%s)\n", 4980 $argKind, $argInfo->getSendByString(), $argInfo->name, 4981 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(), 4982 !$argInfo->isVariadic ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 4983 ); 4984 } else { 4985 $code .= sprintf( 4986 "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n", 4987 $argKind, $argInfo->getSendByString(), $argInfo->name, 4988 $arginfoType->toTypeMask(), 4989 $argInfo->getDefaultValueAsArginfoString() 4990 ); 4991 } 4992 } 4993 } else { 4994 $code .= sprintf( 4995 "\tZEND_%s_INFO%s(%s, %s%s)\n", 4996 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 4997 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 4998 ); 4999 } 5000 } 5001 5002 $code .= "ZEND_END_ARG_INFO()"; 5003 return $code . "\n"; 5004} 5005 5006/** @param FuncInfo[] $generatedFuncInfos */ 5007function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo { 5008 foreach ($generatedFuncInfos as $generatedFuncInfo) { 5009 if ($generatedFuncInfo->equalsApartFromNameAndRefcount($funcInfo)) { 5010 return $generatedFuncInfo; 5011 } 5012 } 5013 return null; 5014} 5015 5016/** 5017 * @template T 5018 * @param iterable<T> $infos 5019 * @param Closure(T): string|null $codeGenerator 5020 * @param ?string $parentCond 5021 */ 5022function generateCodeWithConditions( 5023 iterable $infos, string $separator, Closure $codeGenerator, ?string $parentCond = null): string { 5024 $code = ""; 5025 5026 // For combining the conditional blocks of the infos with the same condition 5027 $openCondition = null; 5028 foreach ($infos as $info) { 5029 $infoCode = $codeGenerator($info); 5030 if ($infoCode === null) { 5031 continue; 5032 } 5033 5034 if ($info->cond && $info->cond !== $parentCond) { 5035 if ($openCondition !== null 5036 && $info->cond !== $openCondition 5037 ) { 5038 // Changing condition, end old 5039 $code .= "#endif\n"; 5040 $code .= $separator; 5041 $code .= "#if {$info->cond}\n"; 5042 $openCondition = $info->cond; 5043 } elseif ($openCondition === null) { 5044 // New condition with no existing one 5045 $code .= $separator; 5046 $code .= "#if {$info->cond}\n"; 5047 $openCondition = $info->cond; 5048 } else { 5049 // Staying in the same condition 5050 $code .= $separator; 5051 } 5052 $code .= $infoCode; 5053 } else { 5054 if ($openCondition !== null) { 5055 // Ending the condition 5056 $code .= "#endif\n"; 5057 $openCondition = null; 5058 } 5059 $code .= $separator; 5060 $code .= $infoCode; 5061 } 5062 } 5063 // The last info might have been in a conditional block 5064 if ($openCondition !== null) { 5065 $code .= "#endif\n"; 5066 } 5067 5068 return $code; 5069} 5070 5071/** 5072 * @param array<string, ConstInfo> $allConstInfos 5073 */ 5074function generateArgInfoCode( 5075 string $stubFilenameWithoutExtension, 5076 FileInfo $fileInfo, 5077 array $allConstInfos, 5078 string $stubHash 5079): string { 5080 $code = "/* This is a generated file, edit the .stub.php file instead.\n" 5081 . " * Stub hash: $stubHash */\n"; 5082 5083 $generatedFuncInfos = []; 5084 5085 $argInfoCode = generateCodeWithConditions( 5086 $fileInfo->getAllFuncInfos(), "\n", 5087 static function (FuncInfo $funcInfo) use (&$generatedFuncInfos, $fileInfo) { 5088 /* If there already is an equivalent arginfo structure, only emit a #define */ 5089 if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) { 5090 $code = sprintf( 5091 "#define %s %s\n", 5092 $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() 5093 ); 5094 } else { 5095 $code = funcInfoToCode($fileInfo, $funcInfo); 5096 } 5097 5098 $generatedFuncInfos[] = $funcInfo; 5099 return $code; 5100 } 5101 ); 5102 5103 if ($argInfoCode !== "") { 5104 $code .= "$argInfoCode\n"; 5105 } 5106 5107 if ($fileInfo->generateFunctionEntries) { 5108 $framelessFunctionCode = generateCodeWithConditions( 5109 $fileInfo->getAllFuncInfos(), "\n", 5110 static function (FuncInfo $funcInfo) { 5111 $code = $funcInfo->getFramelessDeclaration($funcInfo); 5112 return $code; 5113 } 5114 ); 5115 5116 if ($framelessFunctionCode !== "") { 5117 $code .= "$framelessFunctionCode\n"; 5118 } 5119 5120 $generatedFunctionDeclarations = []; 5121 $code .= generateCodeWithConditions( 5122 $fileInfo->getAllFuncInfos(), "", 5123 static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarations) { 5124 $key = $funcInfo->getDeclarationKey(); 5125 if (isset($generatedFunctionDeclarations[$key])) { 5126 return null; 5127 } 5128 5129 $generatedFunctionDeclarations[$key] = true; 5130 return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); 5131 } 5132 ); 5133 5134 $code .= generateFunctionEntries(null, $fileInfo->funcInfos); 5135 5136 foreach ($fileInfo->classInfos as $classInfo) { 5137 $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos, $classInfo->cond); 5138 } 5139 } 5140 5141 $php80MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_80_VERSION_ID; 5142 5143 if ($fileInfo->generateClassEntries) { 5144 if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->getMinimumPhpVersionIdCompatibility(), null)) { 5145 if (!$php80MinimumCompatibility) { 5146 $attributeInitializationCode = "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")" . $attributeInitializationCode . "#endif\n"; 5147 } 5148 } 5149 5150 if ($attributeInitializationCode !== "" || !empty($fileInfo->constInfos)) { 5151 $code .= "\nstatic void register_{$stubFilenameWithoutExtension}_symbols(int module_number)\n"; 5152 $code .= "{\n"; 5153 5154 foreach ($fileInfo->constInfos as $constInfo) { 5155 $code .= $constInfo->getDeclaration($allConstInfos); 5156 } 5157 5158 if ($attributeInitializationCode !== "" && $fileInfo->constInfos) { 5159 $code .= "\n"; 5160 } 5161 5162 $code .= $attributeInitializationCode; 5163 $code .= "}\n"; 5164 } 5165 5166 $code .= generateClassEntryCode($fileInfo, $allConstInfos); 5167 } 5168 5169 return $code; 5170} 5171 5172/** @param array<string, ConstInfo> $allConstInfos */ 5173function generateClassEntryCode(FileInfo $fileInfo, array $allConstInfos): string { 5174 $code = ""; 5175 5176 foreach ($fileInfo->classInfos as $class) { 5177 $code .= "\n" . $class->getRegistration($allConstInfos); 5178 } 5179 5180 return $code; 5181} 5182 5183/** @param FuncInfo[] $funcInfos */ 5184function generateFunctionEntries(?Name $className, array $funcInfos, ?string $cond = null): string { 5185 // No need to add anything if there are no function entries 5186 if ($funcInfos === []) { 5187 return ''; 5188 } 5189 5190 $code = "\n"; 5191 5192 if ($cond) { 5193 $code .= "#if {$cond}\n"; 5194 } 5195 5196 $functionEntryName = "ext_functions"; 5197 if ($className) { 5198 $underscoreName = implode("_", $className->getParts()); 5199 $functionEntryName = "class_{$underscoreName}_methods"; 5200 } 5201 5202 $code .= "static const zend_function_entry {$functionEntryName}[] = {\n"; 5203 $code .= generateCodeWithConditions($funcInfos, "", static function (FuncInfo $funcInfo) { 5204 return $funcInfo->getFunctionEntry(); 5205 }, $cond); 5206 $code .= "\tZEND_FE_END\n"; 5207 $code .= "};\n"; 5208 5209 if ($cond) { 5210 $code .= "#endif\n"; 5211 } 5212 5213 return $code; 5214} 5215 5216/** @param iterable<FuncInfo> $funcInfos */ 5217function generateFunctionAttributeInitialization(iterable $funcInfos, array $allConstInfos, ?int $phpVersionIdMinimumCompatibility, ?string $parentCond = null): string { 5218 return generateCodeWithConditions( 5219 $funcInfos, 5220 "", 5221 static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility) { 5222 $code = null; 5223 5224 if ($funcInfo->name instanceof MethodName) { 5225 $functionTable = "&class_entry->function_table"; 5226 } else { 5227 $functionTable = "CG(function_table)"; 5228 } 5229 5230 foreach ($funcInfo->attributes as $key => $attribute) { 5231 $code .= $attribute->generateCode( 5232 "zend_add_function_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1)", 5233 "func_" . $funcInfo->name->getNameForAttributes() . "_$key", 5234 $allConstInfos, 5235 $phpVersionIdMinimumCompatibility 5236 ); 5237 } 5238 5239 foreach ($funcInfo->args as $index => $arg) { 5240 foreach ($arg->attributes as $key => $attribute) { 5241 $code .= $attribute->generateCode( 5242 "zend_add_parameter_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1), $index", 5243 "func_{$funcInfo->name->getNameForAttributes()}_arg{$index}_$key", 5244 $allConstInfos, 5245 $phpVersionIdMinimumCompatibility 5246 ); 5247 } 5248 } 5249 5250 return $code; 5251 }, 5252 $parentCond 5253 ); 5254} 5255 5256/** 5257 * @param iterable<ConstInfo> $constInfos 5258 * @param array<string, ConstInfo> $allConstInfos 5259 */ 5260function generateConstantAttributeInitialization( 5261 iterable $constInfos, 5262 array $allConstInfos, 5263 ?int $phpVersionIdMinimumCompatibility, 5264 ?string $parentCond = null 5265): string { 5266 return generateCodeWithConditions( 5267 $constInfos, 5268 "", 5269 static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility) { 5270 $code = null; 5271 5272 foreach ($constInfo->attributes as $key => $attribute) { 5273 $code .= $attribute->generateCode( 5274 "zend_add_class_constant_attribute(class_entry, const_" . $constInfo->name->getDeclarationName(), 5275 "const_" . $constInfo->name->getDeclarationName() . "_$key", 5276 $allConstInfos, 5277 $phpVersionIdMinimumCompatibility 5278 ); 5279 } 5280 5281 return $code; 5282 }, 5283 $parentCond 5284 ); 5285} 5286 5287/** 5288 * @param iterable<PropertyInfo> $propertyInfos 5289 * @param array<string, ConstInfo> $allConstInfos 5290 */ 5291function generatePropertyAttributeInitialization( 5292 iterable $propertyInfos, 5293 array $allConstInfos, 5294 ?int $phpVersionIdMinimumCompatibility 5295): string { 5296 $code = ""; 5297 foreach ($propertyInfos as $propertyInfo) { 5298 foreach ($propertyInfo->attributes as $key => $attribute) { 5299 $code .= $attribute->generateCode( 5300 "zend_add_property_attribute(class_entry, property_" . $propertyInfo->name->getDeclarationName(), 5301 "property_" . $propertyInfo->name->getDeclarationName() . "_" . $key, 5302 $allConstInfos, 5303 $phpVersionIdMinimumCompatibility 5304 ); 5305 } 5306 } 5307 5308 return $code; 5309} 5310 5311/** @param array<string, FuncInfo> $funcMap */ 5312function generateOptimizerInfo(array $funcMap): string { 5313 5314 $code = "/* This is a generated file, edit the .stub.php files instead. */\n\n"; 5315 5316 $code .= "static const func_info_t func_infos[] = {\n"; 5317 5318 $code .= generateCodeWithConditions($funcMap, "", static function (FuncInfo $funcInfo) { 5319 return $funcInfo->getOptimizerInfo(); 5320 }); 5321 5322 $code .= "};\n"; 5323 5324 return $code; 5325} 5326 5327/** 5328 * @param array<int, string[]> $flagsByPhpVersions 5329 * @return string[] 5330 */ 5331function generateVersionDependentFlagCode(string $codeTemplate, array $flagsByPhpVersions, ?int $phpVersionIdMinimumCompatibility): array 5332{ 5333 $phpVersions = ALL_PHP_VERSION_IDS; 5334 sort($phpVersions); 5335 $currentPhpVersion = end($phpVersions); 5336 5337 // No version compatibility is needed 5338 if ($phpVersionIdMinimumCompatibility === null) { 5339 if (empty($flagsByPhpVersions[$currentPhpVersion])) { 5340 return []; 5341 } 5342 5343 return [sprintf($codeTemplate, implode("|", $flagsByPhpVersions[$currentPhpVersion]))]; 5344 } 5345 5346 // Remove flags which depend on a PHP version below the minimally supported one 5347 ksort($flagsByPhpVersions); 5348 $index = array_search($phpVersionIdMinimumCompatibility, array_keys($flagsByPhpVersions)); 5349 if ($index === false) { 5350 throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMinimumCompatibility\""); 5351 } 5352 $flagsByPhpVersions = array_slice($flagsByPhpVersions, $index, null, true); 5353 5354 // Remove empty version-specific flags 5355 $flagsByPhpVersions = array_filter( 5356 $flagsByPhpVersions, 5357 static function (array $value): bool { 5358 return !empty($value); 5359 }); 5360 5361 // There are no version-specific flags 5362 if (empty($flagsByPhpVersions)) { 5363 return []; 5364 } 5365 5366 // Remove version-specific flags which don't differ from the previous one 5367 $previousVersionId = null; 5368 foreach ($flagsByPhpVersions as $versionId => $versionFlags) { 5369 if ($previousVersionId !== null && $flagsByPhpVersions[$previousVersionId] === $versionFlags) { 5370 unset($flagsByPhpVersions[$versionId]); 5371 } else { 5372 $previousVersionId = $versionId; 5373 } 5374 } 5375 5376 $flagCount = count($flagsByPhpVersions); 5377 5378 // Do not add a condition unnecessarily when the only version is the same as the minimally supported one 5379 if ($flagCount === 1) { 5380 reset($flagsByPhpVersions); 5381 $firstVersion = key($flagsByPhpVersions); 5382 if ($firstVersion === $phpVersionIdMinimumCompatibility) { 5383 return [sprintf($codeTemplate, implode("|", reset($flagsByPhpVersions)))]; 5384 } 5385 } 5386 5387 // Add the necessary conditions around the code using the version-specific flags 5388 $result = []; 5389 $i = 0; 5390 foreach (array_reverse($flagsByPhpVersions, true) as $version => $versionFlags) { 5391 $code = ""; 5392 5393 $if = $i === 0 ? "#if" : "#elif"; 5394 $endif = $i === $flagCount - 1 ? "#endif\n" : ""; 5395 5396 $code .= "$if (PHP_VERSION_ID >= $version)\n"; 5397 5398 $code .= sprintf($codeTemplate, implode("|", $versionFlags)); 5399 $code .= $endif; 5400 5401 $result[] = $code; 5402 $i++; 5403 } 5404 5405 return $result; 5406} 5407 5408/** 5409 * @param array<string, ConstInfo> $constMap 5410 * @param array<string, ConstInfo> $undocumentedConstMap 5411 * @return array<string, string|null> 5412 */ 5413function replacePredefinedConstants(string $targetDirectory, array $constMap, array &$undocumentedConstMap): array { 5414 /** @var array<string, string> $documentedConstMap */ 5415 $documentedConstMap = []; 5416 /** @var array<string, string> $predefinedConstants */ 5417 $predefinedConstants = []; 5418 5419 $it = new RecursiveIteratorIterator( 5420 new RecursiveDirectoryIterator($targetDirectory), 5421 RecursiveIteratorIterator::LEAVES_ONLY 5422 ); 5423 5424 foreach ($it as $file) { 5425 $pathName = $file->getPathName(); 5426 if (!preg_match('/(?:[\w\.]*constants[\w\._]*|tokens).xml$/i', basename($pathName))) { 5427 continue; 5428 } 5429 5430 $xml = file_get_contents($pathName); 5431 if ($xml === false) { 5432 continue; 5433 } 5434 5435 if (stripos($xml, "<appendix") === false && stripos($xml, "<sect2") === false && 5436 stripos($xml, "<chapter") === false && stripos($xml, 'role="constant_list"') === false 5437 ) { 5438 continue; 5439 } 5440 5441 $replacedXml = getReplacedSynopsisXml($xml); 5442 5443 $doc = new DOMDocument(); 5444 $doc->formatOutput = false; 5445 $doc->preserveWhiteSpace = true; 5446 $doc->validateOnParse = true; 5447 $success = $doc->loadXML($replacedXml); 5448 if (!$success) { 5449 echo "Failed opening $pathName\n"; 5450 continue; 5451 } 5452 5453 $updated = false; 5454 5455 foreach ($doc->getElementsByTagName("varlistentry") as $entry) { 5456 if (!$entry instanceof DOMElement) { 5457 continue; 5458 } 5459 5460 foreach ($entry->getElementsByTagName("term") as $manualTermElement) { 5461 $manualConstantElement = $manualTermElement->getElementsByTagName("constant")->item(0); 5462 if (!$manualConstantElement instanceof DOMElement) { 5463 continue; 5464 } 5465 5466 $manualConstantName = $manualConstantElement->textContent; 5467 5468 $stubConstant = $constMap[$manualConstantName] ?? null; 5469 if ($stubConstant === null) { 5470 continue; 5471 } 5472 5473 $documentedConstMap[$manualConstantName] = $manualConstantName; 5474 5475 if ($entry->firstChild instanceof DOMText) { 5476 $indentationLevel = strlen(str_replace("\n", "", $entry->firstChild->textContent)); 5477 } else { 5478 $indentationLevel = 3; 5479 } 5480 $newTermElement = $stubConstant->getPredefinedConstantTerm($doc, $indentationLevel); 5481 5482 if ($manualTermElement->textContent === $newTermElement->textContent) { 5483 continue; 5484 } 5485 5486 $manualTermElement->parentNode->replaceChild($newTermElement, $manualTermElement); 5487 $updated = true; 5488 } 5489 } 5490 5491 foreach ($doc->getElementsByTagName("row") as $row) { 5492 if (!$row instanceof DOMElement) { 5493 continue; 5494 } 5495 5496 $entry = $row->getElementsByTagName("entry")->item(0); 5497 if (!$entry instanceof DOMElement) { 5498 continue; 5499 } 5500 5501 foreach ($entry->getElementsByTagName("constant") as $manualConstantElement) { 5502 if (!$manualConstantElement instanceof DOMElement) { 5503 continue; 5504 } 5505 5506 $manualConstantName = $manualConstantElement->textContent; 5507 5508 $stubConstant = $constMap[$manualConstantName] ?? null; 5509 if ($stubConstant === null) { 5510 continue; 5511 } 5512 5513 $documentedConstMap[$manualConstantName] = $manualConstantName; 5514 5515 if ($row->firstChild instanceof DOMText) { 5516 $indentationLevel = strlen(str_replace("\n", "", $row->firstChild->textContent)); 5517 } else { 5518 $indentationLevel = 3; 5519 } 5520 $newEntryElement = $stubConstant->getPredefinedConstantEntry($doc, $indentationLevel); 5521 5522 if ($entry->textContent === $newEntryElement->textContent) { 5523 continue; 5524 } 5525 5526 $entry->parentNode->replaceChild($newEntryElement, $entry); 5527 $updated = true; 5528 } 5529 } 5530 5531 if ($updated) { 5532 $replacedXml = $doc->saveXML(); 5533 5534 $replacedXml = preg_replace( 5535 [ 5536 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 5537 '/<appendix\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5538 '/<appendix\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5539 '/<sect2\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5540 '/<sect2\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5541 '/<chapter\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5542 '/<chapter\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5543 ], 5544 [ 5545 "&$1", 5546 "<appendix xml:id=\"$2\" xmlns=\"$1\">", 5547 "<appendix xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 5548 "<sect2 xml:id=\"$2\" xmlns=\"$1\">", 5549 "<sect2 xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 5550 "<chapter xml:id=\"$2\" xmlns=\"$1\">", 5551 "<chapter xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 5552 ], 5553 $replacedXml 5554 ); 5555 5556 $predefinedConstants[$pathName] = $replacedXml; 5557 } 5558 } 5559 5560 $undocumentedConstMap = array_diff_key($constMap, $documentedConstMap); 5561 5562 return $predefinedConstants; 5563} 5564 5565/** 5566 * @param array<string, ClassInfo> $classMap 5567 * @param array<string, ConstInfo> $allConstInfos 5568 * @return array<string, string> 5569 */ 5570function generateClassSynopses(array $classMap, array $allConstInfos): array { 5571 $result = []; 5572 5573 foreach ($classMap as $classInfo) { 5574 $classSynopsis = $classInfo->getClassSynopsisDocument($classMap, $allConstInfos); 5575 if ($classSynopsis !== null) { 5576 $result[ClassInfo::getClassSynopsisFilename($classInfo->name) . ".xml"] = $classSynopsis; 5577 } 5578 } 5579 5580 return $result; 5581} 5582 5583/** 5584 * @param array<string, ClassInfo> $classMap 5585 * @param array<string, ConstInfo> $allConstInfos 5586 * @param array<string, ClassInfo> $undocumentedClassMap 5587 * @return array<string, string> 5588 */ 5589function replaceClassSynopses( 5590 string $targetDirectory, 5591 array $classMap, 5592 array $allConstInfos, 5593 array &$undocumentedClassMap 5594): array { 5595 /** @var array<string, string> $documentedClassMap */ 5596 $documentedClassMap = []; 5597 /** @var array<string, string> $classSynopses */ 5598 $classSynopses = []; 5599 5600 $it = new RecursiveIteratorIterator( 5601 new RecursiveDirectoryIterator($targetDirectory), 5602 RecursiveIteratorIterator::LEAVES_ONLY 5603 ); 5604 5605 foreach ($it as $file) { 5606 $pathName = $file->getPathName(); 5607 if (!preg_match('/\.xml$/i', $pathName)) { 5608 continue; 5609 } 5610 5611 $xml = file_get_contents($pathName); 5612 if ($xml === false) { 5613 continue; 5614 } 5615 5616 if (stripos($xml, "<classsynopsis") === false) { 5617 continue; 5618 } 5619 5620 $replacedXml = getReplacedSynopsisXml($xml); 5621 5622 $doc = new DOMDocument(); 5623 $doc->formatOutput = false; 5624 $doc->preserveWhiteSpace = true; 5625 $doc->validateOnParse = true; 5626 $success = $doc->loadXML($replacedXml); 5627 if (!$success) { 5628 echo "Failed opening $pathName\n"; 5629 continue; 5630 } 5631 5632 $classSynopsisElements = []; 5633 foreach ($doc->getElementsByTagName("classsynopsis") as $element) { 5634 $classSynopsisElements[] = $element; 5635 } 5636 5637 foreach ($classSynopsisElements as $classSynopsis) { 5638 if (!$classSynopsis instanceof DOMElement) { 5639 continue; 5640 } 5641 5642 $child = $classSynopsis->firstElementChild; 5643 if ($child === null) { 5644 continue; 5645 } 5646 $child = $child->lastElementChild; 5647 if ($child === null) { 5648 continue; 5649 } 5650 $className = $child->textContent; 5651 if (!isset($classMap[$className])) { 5652 continue; 5653 } 5654 5655 $documentedClassMap[$className] = $className; 5656 5657 $classInfo = $classMap[$className]; 5658 5659 $newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap, $allConstInfos); 5660 if ($newClassSynopsis === null) { 5661 continue; 5662 } 5663 5664 // Check if there is any change - short circuit if there is not any. 5665 5666 if (replaceAndCompareXmls($doc, $classSynopsis, $newClassSynopsis)) { 5667 continue; 5668 } 5669 5670 // Return the updated XML 5671 5672 $replacedXml = $doc->saveXML(); 5673 5674 $replacedXml = preg_replace( 5675 [ 5676 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 5677 '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5678 '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5679 '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5680 '/<reference\s+role="(\w+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5681 '/<reference\s+xmlns=\"([^"]+)\"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+role="(\w+)"\s+xml:id="([^"]+)"\s*>/i', 5682 '/<reference\s+xmlns=\"([^"]+)\"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s+role="(\w+)"\s*>/i', 5683 ], 5684 [ 5685 "&$1", 5686 "<reference xml:id=\"$3\" role=\"$1\" xmlns=\"$2\">", 5687 "<reference xml:id=\"$4\" role=\"$1\" xmlns=\"$2\" xmlns:xi=\"$3\">", 5688 "<reference xml:id=\"$5\" role=\"$1\" xmlns=\"$2\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">", 5689 "<reference xml:id=\"$5\" role=\"$1\" xmlns=\"$4\" xmlns:xlink=\"$2\" xmlns:xi=\"$2\">", 5690 "<reference xml:id=\"$5\" role=\"$4\" xmlns=\"$1\" xmlns:xlink=\"$2\" xmlns:xi=\"$3\">", 5691 "<reference xml:id=\"$4\" role=\"$5\" xmlns=\"$1\" xmlns:xlink=\"$2\" xmlns:xi=\"$3\">", 5692 ], 5693 $replacedXml 5694 ); 5695 5696 $classSynopses[$pathName] = $replacedXml; 5697 } 5698 } 5699 5700 $undocumentedClassMap = array_diff_key($classMap, $documentedClassMap); 5701 5702 return $classSynopses; 5703} 5704 5705function getReplacedSynopsisXml(string $xml): string 5706{ 5707 return preg_replace( 5708 [ 5709 "/&([A-Za-z0-9._{}%-]+?;)/", 5710 "/<(\/)*xi:([A-Za-z]+?)/" 5711 ], 5712 [ 5713 "REPLACED-ENTITY-$1", 5714 "<$1XI$2", 5715 ], 5716 $xml 5717 ); 5718} 5719 5720/** 5721 * @param array<string, FuncInfo> $funcMap 5722 * @param array<string, FuncInfo> $aliasMap 5723 * @return array<string, string> 5724 */ 5725function generateMethodSynopses(array $funcMap, array $aliasMap): array { 5726 $result = []; 5727 5728 foreach ($funcMap as $funcInfo) { 5729 $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap); 5730 if ($methodSynopsis !== null) { 5731 $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis; 5732 } 5733 } 5734 5735 return $result; 5736} 5737 5738/** 5739 * @param array<string, FuncInfo> $funcMap 5740 * @param array<string, FuncInfo> $aliasMap 5741 * @param array<int, string> $methodSynopsisWarnings 5742 * @param array<string, FuncInfo> $undocumentedFuncMap 5743 * @return array<string, string> 5744 */ 5745function replaceMethodSynopses( 5746 string $targetDirectory, 5747 array $funcMap, 5748 array $aliasMap, 5749 bool $isVerifyManual, 5750 array &$methodSynopsisWarnings, 5751 array &$undocumentedFuncMap 5752): array { 5753 /** @var array<string, string> $documentedFuncMap */ 5754 $documentedFuncMap = []; 5755 /** @var array<string, string> $methodSynopses */ 5756 $methodSynopses = []; 5757 5758 $it = new RecursiveIteratorIterator( 5759 new RecursiveDirectoryIterator($targetDirectory), 5760 RecursiveIteratorIterator::LEAVES_ONLY 5761 ); 5762 5763 foreach ($it as $file) { 5764 $pathName = $file->getPathName(); 5765 if (!preg_match('/\.xml$/i', $pathName)) { 5766 continue; 5767 } 5768 5769 $xml = file_get_contents($pathName); 5770 if ($xml === false) { 5771 continue; 5772 } 5773 5774 if ($isVerifyManual) { 5775 $matches = []; 5776 preg_match("/<refname>\s*([\w:]+)\s*<\/refname>\s*<refpurpose>\s*&Alias;\s*<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>\s*<\/refpurpose>/i", $xml, $matches); 5777 $aliasName = $matches[1] ?? null; 5778 $alias = $funcMap[$aliasName] ?? null; 5779 $funcName = $matches[2] ?? null; 5780 $func = $funcMap[$funcName] ?? null; 5781 5782 if ($alias && 5783 !$alias->isUndocumentable && 5784 ($func === null || $func->alias === null || $func->alias->__toString() !== $aliasName) && 5785 ($alias->alias === null || $alias->alias->__toString() !== $funcName) 5786 ) { 5787 $methodSynopsisWarnings[] = "$aliasName()" . ($alias->alias ? " is an alias of " . $alias->alias->__toString() . "(), but it" : "") . " is incorrectly documented as an alias for $funcName()"; 5788 } 5789 5790 $matches = []; 5791 preg_match("/<(?:para|simpara)>\s*(?:&info.function.alias;|&info.method.alias;|&Alias;)\s+<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>/i", $xml, $matches); 5792 $descriptionFuncName = $matches[1] ?? null; 5793 $descriptionFunc = $funcMap[$descriptionFuncName] ?? null; 5794 if ($descriptionFunc && $funcName !== $descriptionFuncName) { 5795 $methodSynopsisWarnings[] = "Alias in the method synopsis description of $pathName doesn't match the alias in the <refpurpose>"; 5796 } 5797 5798 if ($aliasName) { 5799 $documentedFuncMap[$aliasName] = $aliasName; 5800 } 5801 } 5802 5803 if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) { 5804 continue; 5805 } 5806 5807 $replacedXml = getReplacedSynopsisXml($xml); 5808 5809 $doc = new DOMDocument(); 5810 $doc->formatOutput = false; 5811 $doc->preserveWhiteSpace = true; 5812 $doc->validateOnParse = true; 5813 $success = $doc->loadXML($replacedXml); 5814 if (!$success) { 5815 echo "Failed opening $pathName\n"; 5816 continue; 5817 } 5818 5819 $methodSynopsisElements = []; 5820 foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) { 5821 $methodSynopsisElements[] = $element; 5822 } 5823 foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) { 5824 $methodSynopsisElements[] = $element; 5825 } 5826 foreach ($doc->getElementsByTagName("methodsynopsis") as $element) { 5827 $methodSynopsisElements[] = $element; 5828 } 5829 5830 foreach ($methodSynopsisElements as $methodSynopsis) { 5831 if (!$methodSynopsis instanceof DOMElement) { 5832 continue; 5833 } 5834 5835 $item = $methodSynopsis->getElementsByTagName("methodname")->item(0); 5836 if (!$item instanceof DOMElement) { 5837 continue; 5838 } 5839 $funcName = $item->textContent; 5840 if (!isset($funcMap[$funcName])) { 5841 continue; 5842 } 5843 5844 $funcInfo = $funcMap[$funcName]; 5845 $documentedFuncMap[$funcInfo->name->__toString()] = $funcInfo->name->__toString(); 5846 5847 $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 5848 if ($newMethodSynopsis === null) { 5849 continue; 5850 } 5851 5852 // Retrieve current signature 5853 5854 $params = []; 5855 $list = $methodSynopsis->getElementsByTagName("methodparam"); 5856 foreach ($list as $i => $item) { 5857 if (!$item instanceof DOMElement) { 5858 continue; 5859 } 5860 5861 $paramList = $item->getElementsByTagName("parameter"); 5862 if ($paramList->count() !== 1) { 5863 continue; 5864 } 5865 5866 $paramName = $paramList->item(0)->textContent; 5867 $paramTypes = []; 5868 5869 $paramList = $item->getElementsByTagName("type"); 5870 foreach ($paramList as $type) { 5871 if (!$type instanceof DOMElement) { 5872 continue; 5873 } 5874 5875 $paramTypes[] = $type->textContent; 5876 } 5877 5878 $params[$paramName] = ["index" => $i, "type" => $paramTypes]; 5879 } 5880 5881 // Check if there is any change - short circuit if there is not any. 5882 5883 if (replaceAndCompareXmls($doc, $methodSynopsis, $newMethodSynopsis)) { 5884 continue; 5885 } 5886 5887 // Update parameter references 5888 5889 $paramList = $doc->getElementsByTagName("parameter"); 5890 /** @var DOMElement $paramElement */ 5891 foreach ($paramList as $paramElement) { 5892 if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") { 5893 continue; 5894 } 5895 5896 $name = $paramElement->textContent; 5897 if (!isset($params[$name])) { 5898 continue; 5899 } 5900 5901 $index = $params[$name]["index"]; 5902 if (!isset($funcInfo->args[$index])) { 5903 continue; 5904 } 5905 5906 $paramElement->textContent = $funcInfo->args[$index]->name; 5907 } 5908 5909 // Return the updated XML 5910 5911 $replacedXml = $doc->saveXML(); 5912 5913 $replacedXml = preg_replace( 5914 [ 5915 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 5916 '/<refentry\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5917 '/<refentry\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i', 5918 ], 5919 [ 5920 "&$1", 5921 "<refentry xml:id=\"$2\" xmlns=\"$1\">", 5922 "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 5923 ], 5924 $replacedXml 5925 ); 5926 5927 $methodSynopses[$pathName] = $replacedXml; 5928 } 5929 } 5930 5931 $undocumentedFuncMap = array_diff_key($funcMap, $documentedFuncMap); 5932 5933 return $methodSynopses; 5934} 5935 5936function replaceAndCompareXmls(DOMDocument $doc, DOMElement $originalSynopsis, DOMElement $newSynopsis): bool 5937{ 5938 $docComparator = new DOMDocument(); 5939 $docComparator->preserveWhiteSpace = false; 5940 $docComparator->formatOutput = true; 5941 5942 $xml1 = $doc->saveXML($originalSynopsis); 5943 $xml1 = getReplacedSynopsisXml($xml1); 5944 $docComparator->loadXML($xml1); 5945 $xml1 = $docComparator->saveXML(); 5946 5947 $originalSynopsis->parentNode->replaceChild($newSynopsis, $originalSynopsis); 5948 5949 $xml2 = $doc->saveXML($newSynopsis); 5950 $xml2 = getReplacedSynopsisXml($xml2); 5951 5952 $docComparator->loadXML($xml2); 5953 $xml2 = $docComparator->saveXML(); 5954 5955 return $xml1 === $xml2; 5956} 5957 5958function installPhpParser(string $version, string $phpParserDir) { 5959 $lockFile = __DIR__ . "/PHP-Parser-install-lock"; 5960 $lockFd = fopen($lockFile, 'w+'); 5961 if (!flock($lockFd, LOCK_EX)) { 5962 throw new Exception("Failed to acquire installation lock"); 5963 } 5964 5965 try { 5966 // Check whether a parallel process has already installed PHP-Parser. 5967 if (is_dir($phpParserDir)) { 5968 return; 5969 } 5970 5971 $cwd = getcwd(); 5972 chdir(__DIR__); 5973 5974 $tarName = "v$version.tar.gz"; 5975 passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 5976 if ($exit !== 0) { 5977 passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 5978 } 5979 if ($exit !== 0) { 5980 throw new Exception("Failed to download PHP-Parser tarball"); 5981 } 5982 if (!mkdir($phpParserDir)) { 5983 throw new Exception("Failed to create directory $phpParserDir"); 5984 } 5985 passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit); 5986 if ($exit !== 0) { 5987 throw new Exception("Failed to extract PHP-Parser tarball"); 5988 } 5989 unlink(__DIR__ . "/$tarName"); 5990 chdir($cwd); 5991 } finally { 5992 flock($lockFd, LOCK_UN); 5993 @unlink($lockFile); 5994 } 5995} 5996 5997function initPhpParser() { 5998 static $isInitialized = false; 5999 if ($isInitialized) { 6000 return; 6001 } 6002 6003 if (!extension_loaded("tokenizer")) { 6004 throw new Exception("The \"tokenizer\" extension is not available"); 6005 } 6006 6007 $isInitialized = true; 6008 $version = "5.0.0"; 6009 $phpParserDir = __DIR__ . "/PHP-Parser-$version"; 6010 if (!is_dir($phpParserDir)) { 6011 installPhpParser($version, $phpParserDir); 6012 } 6013 6014 spl_autoload_register(static function(string $class) use ($phpParserDir) { 6015 if (strpos($class, "PhpParser\\") === 0) { 6016 $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php"; 6017 require $fileName; 6018 } 6019 }); 6020} 6021 6022$optind = null; 6023$options = getopt( 6024 "fh", 6025 [ 6026 "force-regeneration", "parameter-stats", "help", "verify", "verify-manual", "replace-predefined-constants", 6027 "generate-classsynopses", "replace-classsynopses", "generate-methodsynopses", "replace-methodsynopses", 6028 "generate-optimizer-info", 6029 ], 6030 $optind 6031); 6032 6033$context = new Context; 6034$printParameterStats = isset($options["parameter-stats"]); 6035$verify = isset($options["verify"]); 6036$verifyManual = isset($options["verify-manual"]); 6037$replacePredefinedConstants = isset($options["replace-predefined-constants"]); 6038$generateClassSynopses = isset($options["generate-classsynopses"]); 6039$replaceClassSynopses = isset($options["replace-classsynopses"]); 6040$generateMethodSynopses = isset($options["generate-methodsynopses"]); 6041$replaceMethodSynopses = isset($options["replace-methodsynopses"]); 6042$generateOptimizerInfo = isset($options["generate-optimizer-info"]); 6043$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); 6044$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $verifyManual || $replacePredefinedConstants || $generateClassSynopses || $generateOptimizerInfo || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses; 6045 6046if (isset($options["h"]) || isset($options["help"])) { 6047 die("\nUsage: gen_stub.php [ -f | --force-regeneration ] [ --replace-predefined-constants ] [ --generate-classsynopses ] [ --replace-classsynopses ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ --verify-manual ] [ --generate-optimizer-info ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n"); 6048} 6049 6050$locations = array_slice($argv, $optind); 6051$locationCount = count($locations); 6052if ($replacePredefinedConstants && $locationCount < 2) { 6053 die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-predefined-constants ./ ../doc-en/\n"); 6054} 6055if ($replaceClassSynopses && $locationCount < 2) { 6056 die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-classsynopses ./ ../doc-en/\n"); 6057} 6058if ($generateMethodSynopses && $locationCount < 2) { 6059 die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --generate-methodsynopses ./ ../doc-en/\n"); 6060} 6061if ($replaceMethodSynopses && $locationCount < 2) { 6062 die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-methodsynopses ./ ../doc-en/\n"); 6063} 6064if ($verifyManual && $locationCount < 2) { 6065 die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --verify-manual ./ ../doc-en/\n"); 6066} 6067$manualTarget = null; 6068if ($replacePredefinedConstants || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses || $verifyManual) { 6069 $manualTarget = array_pop($locations); 6070} 6071if ($locations === []) { 6072 $locations = ['.']; 6073} 6074 6075$fileInfos = []; 6076foreach (array_unique($locations) as $location) { 6077 if (is_file($location)) { 6078 // Generate single file. 6079 $fileInfo = processStubFile($location, $context); 6080 if ($fileInfo) { 6081 $fileInfos[] = $fileInfo; 6082 } 6083 } else if (is_dir($location)) { 6084 array_push($fileInfos, ...processDirectory($location, $context)); 6085 } else { 6086 echo "$location is neither a file nor a directory.\n"; 6087 exit(1); 6088 } 6089} 6090 6091if ($printParameterStats) { 6092 $parameterStats = []; 6093 6094 foreach ($fileInfos as $fileInfo) { 6095 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 6096 foreach ($funcInfo->args as $argInfo) { 6097 if (!isset($parameterStats[$argInfo->name])) { 6098 $parameterStats[$argInfo->name] = 0; 6099 } 6100 $parameterStats[$argInfo->name]++; 6101 } 6102 } 6103 } 6104 6105 arsort($parameterStats); 6106 echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; 6107} 6108 6109/** @var array<string, ClassInfo> $classMap */ 6110$classMap = []; 6111/** @var array<string, FuncInfo> $funcMap */ 6112$funcMap = []; 6113/** @var array<string, FuncInfo> $aliasMap */ 6114$aliasMap = []; 6115 6116/** @var array<string, ConstInfo> $undocumentedConstMap */ 6117$undocumentedConstMap = []; 6118/** @var array<string, ClassInfo> $undocumentedClassMap */ 6119$undocumentedClassMap = []; 6120/** @var array<string, FuncInfo> $undocumentedFuncMap */ 6121$undocumentedFuncMap = []; 6122/** @var array<int, string> $methodSynopsisWarnings */ 6123$methodSynopsisWarnings = []; 6124 6125foreach ($fileInfos as $fileInfo) { 6126 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 6127 $funcMap[$funcInfo->name->__toString()] = $funcInfo; 6128 6129 // TODO: Don't use aliasMap for methodsynopsis? 6130 if ($funcInfo->aliasType === "alias") { 6131 $aliasMap[$funcInfo->alias->__toString()] = $funcInfo; 6132 } 6133 } 6134 6135 foreach ($fileInfo->classInfos as $classInfo) { 6136 $classMap[$classInfo->name->__toString()] = $classInfo; 6137 6138 if ($classInfo->alias !== null) { 6139 $classMap[$classInfo->alias] = $classInfo; 6140 } 6141 } 6142} 6143 6144if ($verify) { 6145 $errors = []; 6146 6147 foreach ($funcMap as $aliasFunc) { 6148 if (!$aliasFunc->alias || $aliasFunc->aliasType !== "alias") { 6149 continue; 6150 } 6151 6152 if (!isset($funcMap[$aliasFunc->alias->__toString()])) { 6153 $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found"; 6154 continue; 6155 } 6156 6157 if (!$aliasFunc->verify) { 6158 continue; 6159 } 6160 6161 $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()]; 6162 $aliasedArgs = $aliasedFunc->args; 6163 $aliasArgs = $aliasFunc->args; 6164 6165 if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) { 6166 if ($aliasFunc->isInstanceMethod()) { 6167 $aliasedArgs = array_slice($aliasedArgs, 1); 6168 } 6169 6170 if ($aliasedFunc->isInstanceMethod()) { 6171 $aliasArgs = array_slice($aliasArgs, 1); 6172 } 6173 } 6174 6175 array_map( 6176 function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { 6177 if ($aliasArg === null) { 6178 assert($aliasedArg !== null); 6179 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; 6180 return null; 6181 } 6182 6183 if ($aliasedArg === null) { 6184 $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; 6185 return null; 6186 } 6187 6188 if ($aliasArg->name !== $aliasedArg->name) { 6189 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; 6190 return null; 6191 } 6192 6193 if ($aliasArg->type != $aliasedArg->type) { 6194 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; 6195 } 6196 6197 if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { 6198 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; 6199 } 6200 }, 6201 $aliasArgs, $aliasedArgs 6202 ); 6203 6204 $aliasedReturn = $aliasedFunc->return; 6205 $aliasReturn = $aliasFunc->return; 6206 6207 if (!$aliasedFunc->name->isConstructor() && !$aliasFunc->name->isConstructor()) { 6208 $aliasedReturnType = $aliasedReturn->type ?? $aliasedReturn->phpDocType; 6209 $aliasReturnType = $aliasReturn->type ?? $aliasReturn->phpDocType; 6210 if ($aliasReturnType != $aliasedReturnType) { 6211 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type"; 6212 } 6213 } 6214 6215 $aliasedPhpDocReturnType = $aliasedReturn->phpDocType; 6216 $aliasPhpDocReturnType = $aliasReturn->phpDocType; 6217 if ($aliasedPhpDocReturnType != $aliasPhpDocReturnType && $aliasedPhpDocReturnType != $aliasReturn->type && $aliasPhpDocReturnType != $aliasedReturn->type) { 6218 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same PHPDoc return type"; 6219 } 6220 } 6221 6222 echo implode("\n", $errors); 6223 if (!empty($errors)) { 6224 echo "\n"; 6225 exit(1); 6226 } 6227} 6228 6229if ($replacePredefinedConstants || $verifyManual) { 6230 $predefinedConstants = replacePredefinedConstants($manualTarget, $context->allConstInfos, $undocumentedConstMap); 6231 6232 if ($replacePredefinedConstants) { 6233 foreach ($predefinedConstants as $filename => $content) { 6234 if (file_put_contents($filename, $content)) { 6235 echo "Saved $filename\n"; 6236 } 6237 } 6238 } 6239} 6240 6241if ($generateClassSynopses) { 6242 $classSynopsesDirectory = getcwd() . "/classsynopses"; 6243 6244 $classSynopses = generateClassSynopses($classMap, $context->allConstInfos); 6245 if (!empty($classSynopses)) { 6246 if (!file_exists($classSynopsesDirectory)) { 6247 mkdir($classSynopsesDirectory); 6248 } 6249 6250 foreach ($classSynopses as $filename => $content) { 6251 if (file_put_contents("$classSynopsesDirectory/$filename", $content)) { 6252 echo "Saved $filename\n"; 6253 } 6254 } 6255 } 6256} 6257 6258if ($replaceClassSynopses || $verifyManual) { 6259 $classSynopses = replaceClassSynopses($manualTarget, $classMap, $context->allConstInfos, $undocumentedClassMap); 6260 6261 if ($replaceClassSynopses) { 6262 foreach ($classSynopses as $filename => $content) { 6263 if (file_put_contents($filename, $content)) { 6264 echo "Saved $filename\n"; 6265 } 6266 } 6267 } 6268} 6269 6270if ($generateMethodSynopses) { 6271 $methodSynopses = generateMethodSynopses($funcMap, $aliasMap); 6272 if (!file_exists($manualTarget)) { 6273 mkdir($manualTarget); 6274 } 6275 6276 foreach ($methodSynopses as $filename => $content) { 6277 if (!file_exists("$manualTarget/$filename")) { 6278 if (file_put_contents("$manualTarget/$filename", $content)) { 6279 echo "Saved $filename\n"; 6280 } 6281 } 6282 } 6283} 6284 6285if ($replaceMethodSynopses || $verifyManual) { 6286 $methodSynopses = replaceMethodSynopses($manualTarget, $funcMap, $aliasMap, $verifyManual, $methodSynopsisWarnings, $undocumentedFuncMap); 6287 6288 if ($replaceMethodSynopses) { 6289 foreach ($methodSynopses as $filename => $content) { 6290 if (file_put_contents($filename, $content)) { 6291 echo "Saved $filename\n"; 6292 } 6293 } 6294 } 6295} 6296 6297if ($generateOptimizerInfo) { 6298 $filename = dirname(__FILE__, 2) . "/Zend/Optimizer/zend_func_infos.h"; 6299 $optimizerInfo = generateOptimizerInfo($funcMap); 6300 6301 if (file_put_contents($filename, $optimizerInfo)) { 6302 echo "Saved $filename\n"; 6303 } 6304} 6305 6306if ($verifyManual) { 6307 foreach ($undocumentedConstMap as $constName => $info) { 6308 if ($info->name->isClassConst() || $info->isUndocumentable) { 6309 continue; 6310 } 6311 6312 echo "Warning: Missing predefined constant for $constName\n"; 6313 } 6314 6315 foreach ($methodSynopsisWarnings as $warning) { 6316 echo "Warning: $warning\n"; 6317 } 6318 6319 foreach ($undocumentedClassMap as $className => $info) { 6320 if (!$info->isUndocumentable) { 6321 echo "Warning: Missing class synopsis for $className\n"; 6322 } 6323 } 6324 6325 foreach ($undocumentedFuncMap as $functionName => $info) { 6326 if (!$info->isUndocumentable) { 6327 echo "Warning: Missing method synopsis for $functionName()\n"; 6328 } 6329 } 6330} 6331