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