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