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