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