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