1#!/usr/bin/env php 2<?php declare(strict_types=1); 3 4use PhpParser\Comment\Doc as DocComment; 5use PhpParser\ConstExprEvaluator; 6use PhpParser\Node; 7use PhpParser\Node\Expr; 8use PhpParser\Node\Name; 9use PhpParser\Node\Stmt; 10use PhpParser\Node\Stmt\Class_; 11use PhpParser\Node\Stmt\Enum_; 12use PhpParser\Node\Stmt\Interface_; 13use PhpParser\Node\Stmt\Trait_; 14use PhpParser\PrettyPrinter\Standard; 15use PhpParser\PrettyPrinterAbstract; 16 17error_reporting(E_ALL); 18 19/** 20 * @return FileInfo[] 21 */ 22function processDirectory(string $dir, Context $context): array { 23 $pathNames = []; 24 $it = new RecursiveIteratorIterator( 25 new RecursiveDirectoryIterator($dir), 26 RecursiveIteratorIterator::LEAVES_ONLY 27 ); 28 foreach ($it as $file) { 29 $pathName = $file->getPathName(); 30 if (preg_match('/\.stub\.php$/', $pathName)) { 31 $pathNames[] = $pathName; 32 } 33 } 34 35 // Make sure stub files are processed in a predictable, system-independent order. 36 sort($pathNames); 37 38 $fileInfos = []; 39 foreach ($pathNames as $pathName) { 40 $fileInfo = processStubFile($pathName, $context); 41 if ($fileInfo) { 42 $fileInfos[] = $fileInfo; 43 } 44 } 45 return $fileInfos; 46} 47 48function processStubFile(string $stubFile, Context $context): ?FileInfo { 49 try { 50 if (!file_exists($stubFile)) { 51 throw new Exception("File $stubFile does not exist"); 52 } 53 54 $arginfoFile = str_replace('.stub.php', '_arginfo.h', $stubFile); 55 $legacyFile = str_replace('.stub.php', '_legacy_arginfo.h', $stubFile); 56 57 $stubCode = file_get_contents($stubFile); 58 $stubHash = computeStubHash($stubCode); 59 $oldStubHash = extractStubHash($arginfoFile); 60 if ($stubHash === $oldStubHash && !$context->forceParse) { 61 /* Stub file did not change, do not regenerate. */ 62 return null; 63 } 64 65 initPhpParser(); 66 $fileInfo = parseStubFile($stubCode); 67 68 $arginfoCode = generateArgInfoCode($fileInfo, $stubHash); 69 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { 70 echo "Saved $arginfoFile\n"; 71 } 72 73 if ($fileInfo->generateLegacyArginfo) { 74 $legacyFileInfo = clone $fileInfo; 75 76 foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) { 77 $funcInfo->discardInfoForOldPhpVersions(); 78 } 79 foreach ($legacyFileInfo->getAllPropertyInfos() as $propertyInfo) { 80 $propertyInfo->discardInfoForOldPhpVersions(); 81 } 82 83 $arginfoCode = generateArgInfoCode($legacyFileInfo, $stubHash); 84 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { 85 echo "Saved $legacyFile\n"; 86 } 87 } 88 89 return $fileInfo; 90 } catch (Exception $e) { 91 echo "In $stubFile:\n{$e->getMessage()}\n"; 92 exit(1); 93 } 94} 95 96function computeStubHash(string $stubCode): string { 97 return sha1(str_replace("\r\n", "\n", $stubCode)); 98} 99 100function extractStubHash(string $arginfoFile): ?string { 101 if (!file_exists($arginfoFile)) { 102 return null; 103 } 104 105 $arginfoCode = file_get_contents($arginfoFile); 106 if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) { 107 return null; 108 } 109 110 return $matches[1]; 111} 112 113class Context { 114 /** @var bool */ 115 public $forceParse = false; 116 /** @var bool */ 117 public $forceRegeneration = false; 118} 119 120class ArrayType extends SimpleType { 121 /** @var Type */ 122 public $keyType; 123 124 /** @var Type */ 125 public $valueType; 126 127 public static function createGenericArray(): self 128 { 129 return new ArrayType(Type::fromString("int|string"), Type::fromString("mixed|ref")); 130 } 131 132 public function __construct(Type $keyType, Type $valueType) 133 { 134 parent::__construct("array", true); 135 136 $this->keyType = $keyType; 137 $this->valueType = $valueType; 138 } 139 140 public function toOptimizerTypeMask(): string { 141 $typeMasks = [ 142 parent::toOptimizerTypeMask(), 143 $this->keyType->toOptimizerTypeMaskForArrayKey(), 144 $this->valueType->toOptimizerTypeMaskForArrayValue(), 145 ]; 146 147 return implode("|", $typeMasks); 148 } 149 150 public function equals(SimpleType $other): bool { 151 if (!parent::equals($other)) { 152 return false; 153 } 154 155 assert(get_class($other) === self::class); 156 157 return Type::equals($this->keyType, $other->keyType) && 158 Type::equals($this->valueType, $other->valueType); 159 } 160} 161 162class SimpleType { 163 /** @var string */ 164 public $name; 165 /** @var bool */ 166 public $isBuiltin; 167 168 public static function fromNode(Node $node): SimpleType { 169 if ($node instanceof Node\Name) { 170 if ($node->toLowerString() === 'static') { 171 // PHP internally considers "static" a builtin type. 172 return new SimpleType($node->toLowerString(), true); 173 } 174 175 if ($node->toLowerString() === 'self') { 176 throw new Exception('The exact class name must be used instead of "self"'); 177 } 178 179 assert($node->isFullyQualified()); 180 return new SimpleType($node->toString(), false); 181 } 182 183 if ($node instanceof Node\Identifier) { 184 if ($node->toLowerString() === 'array') { 185 return ArrayType::createGenericArray(); 186 } 187 188 return new SimpleType($node->toLowerString(), true); 189 } 190 191 throw new Exception("Unexpected node type"); 192 } 193 194 public static function fromString(string $typeString): SimpleType 195 { 196 switch (strtolower($typeString)) { 197 case "void": 198 case "null": 199 case "false": 200 case "true": 201 case "bool": 202 case "int": 203 case "float": 204 case "string": 205 case "callable": 206 case "iterable": 207 case "object": 208 case "resource": 209 case "mixed": 210 case "static": 211 case "never": 212 case "ref": 213 return new SimpleType(strtolower($typeString), true); 214 case "array": 215 return ArrayType::createGenericArray(); 216 case "self": 217 throw new Exception('The exact class name must be used instead of "self"'); 218 } 219 220 $matches = []; 221 $isArray = preg_match("/(.*)\s*\[\s*\]/", $typeString, $matches); 222 if ($isArray) { 223 return new ArrayType(Type::fromString("int"), Type::fromString($matches[1])); 224 } 225 226 $matches = []; 227 $isArray = preg_match("/array\s*<\s*([A-Za-z0-9_-|]+)?(\s*,\s*)?([A-Za-z0-9_-|]+)?\s*>/i", $typeString, $matches); 228 if ($isArray) { 229 if (empty($matches[1]) || empty($matches[3])) { 230 throw new Exception("array<> type hint must have both a key and a value"); 231 } 232 233 return new ArrayType(Type::fromString($matches[1]), Type::fromString($matches[3])); 234 } 235 236 return new SimpleType($typeString, false); 237 } 238 239 public static function null(): SimpleType 240 { 241 return new SimpleType("null", true); 242 } 243 244 public static function void(): SimpleType 245 { 246 return new SimpleType("void", true); 247 } 248 249 protected function __construct(string $name, bool $isBuiltin) { 250 $this->name = $name; 251 $this->isBuiltin = $isBuiltin; 252 } 253 254 public function isScalar(): bool { 255 return $this->isBuiltin && in_array($this->name, ["null", "false", "true", "bool", "int", "float"], true); 256 } 257 258 public function isNull(): bool { 259 return $this->isBuiltin && $this->name === 'null'; 260 } 261 262 public function toTypeCode(): string { 263 assert($this->isBuiltin); 264 switch ($this->name) { 265 case "bool": 266 return "_IS_BOOL"; 267 case "int": 268 return "IS_LONG"; 269 case "float": 270 return "IS_DOUBLE"; 271 case "string": 272 return "IS_STRING"; 273 case "array": 274 return "IS_ARRAY"; 275 case "object": 276 return "IS_OBJECT"; 277 case "void": 278 return "IS_VOID"; 279 case "callable": 280 return "IS_CALLABLE"; 281 case "iterable": 282 return "IS_ITERABLE"; 283 case "mixed": 284 return "IS_MIXED"; 285 case "static": 286 return "IS_STATIC"; 287 case "never": 288 return "IS_NEVER"; 289 default: 290 throw new Exception("Not implemented: $this->name"); 291 } 292 } 293 294 public function toTypeMask(): string { 295 assert($this->isBuiltin); 296 297 switch ($this->name) { 298 case "null": 299 return "MAY_BE_NULL"; 300 case "false": 301 return "MAY_BE_FALSE"; 302 case "bool": 303 return "MAY_BE_BOOL"; 304 case "int": 305 return "MAY_BE_LONG"; 306 case "float": 307 return "MAY_BE_DOUBLE"; 308 case "string": 309 return "MAY_BE_STRING"; 310 case "array": 311 return "MAY_BE_ARRAY"; 312 case "object": 313 return "MAY_BE_OBJECT"; 314 case "callable": 315 return "MAY_BE_CALLABLE"; 316 case "iterable": 317 return "MAY_BE_ITERABLE"; 318 case "mixed": 319 return "MAY_BE_ANY"; 320 case "void": 321 return "MAY_BE_VOID"; 322 case "static": 323 return "MAY_BE_STATIC"; 324 case "never": 325 return "MAY_BE_NEVER"; 326 default: 327 throw new Exception("Not implemented: $this->name"); 328 } 329 } 330 331 public function toOptimizerTypeMaskForArrayKey(): string { 332 assert($this->isBuiltin); 333 334 switch ($this->name) { 335 case "int": 336 return "MAY_BE_ARRAY_KEY_LONG"; 337 case "string": 338 return "MAY_BE_ARRAY_KEY_STRING"; 339 default: 340 throw new Exception("Type $this->name cannot be an array key"); 341 } 342 } 343 344 public function toOptimizerTypeMaskForArrayValue(): string { 345 if (!$this->isBuiltin) { 346 return "MAY_BE_ARRAY_OF_OBJECT"; 347 } 348 349 switch ($this->name) { 350 case "null": 351 return "MAY_BE_ARRAY_OF_NULL"; 352 case "false": 353 return "MAY_BE_ARRAY_OF_FALSE"; 354 case "bool": 355 return "MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE"; 356 case "int": 357 return "MAY_BE_ARRAY_OF_LONG"; 358 case "float": 359 return "MAY_BE_ARRAY_OF_DOUBLE"; 360 case "string": 361 return "MAY_BE_ARRAY_OF_STRING"; 362 case "array": 363 return "MAY_BE_ARRAY_OF_ARRAY"; 364 case "object": 365 return "MAY_BE_ARRAY_OF_OBJECT"; 366 case "resource": 367 return "MAY_BE_ARRAY_OF_RESOURCE"; 368 case "mixed": 369 return "MAY_BE_ARRAY_OF_ANY"; 370 case "ref": 371 return "MAY_BE_ARRAY_OF_REF"; 372 default: 373 throw new Exception("Type $this->name cannot be an array value"); 374 } 375 } 376 377 public function toOptimizerTypeMask(): string { 378 if (!$this->isBuiltin) { 379 return "MAY_BE_OBJECT"; 380 } 381 382 switch ($this->name) { 383 case "true": 384 return "MAY_BE_TRUE"; 385 case "resource": 386 return "MAY_BE_RESOURCE"; 387 case "callable": 388 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"; 389 case "iterable": 390 return "MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_OBJECT"; 391 case "mixed": 392 return "MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY"; 393 } 394 395 return $this->toTypeMask(); 396 } 397 398 public function toEscapedName(): string { 399 return str_replace('\\', '\\\\', $this->name); 400 } 401 402 public function toVarEscapedName(): string { 403 $name = str_replace('_', '__', $this->name); 404 return str_replace('\\', '_', $this->name); 405 } 406 407 public function equals(SimpleType $other): bool { 408 return $this->name === $other->name && $this->isBuiltin === $other->isBuiltin; 409 } 410} 411 412class Type { 413 /** @var SimpleType[] */ 414 public $types; 415 416 public static function fromNode(Node $node): Type { 417 if ($node instanceof Node\UnionType) { 418 return new Type(array_map(['SimpleType', 'fromNode'], $node->types)); 419 } 420 421 if ($node instanceof Node\NullableType) { 422 return new Type( 423 [ 424 SimpleType::fromNode($node->type), 425 SimpleType::null(), 426 ] 427 ); 428 } 429 430 return new Type([SimpleType::fromNode($node)]); 431 } 432 433 public static function fromString(string $typeString): self { 434 $typeString .= "|"; 435 $simpleTypes = []; 436 $simpleTypeOffset = 0; 437 $inArray = false; 438 439 $typeStringLength = strlen($typeString); 440 for ($i = 0; $i < $typeStringLength; $i++) { 441 $char = $typeString[$i]; 442 443 if ($char === "<") { 444 $inArray = true; 445 continue; 446 } 447 448 if ($char === ">") { 449 $inArray = false; 450 continue; 451 } 452 453 if ($inArray) { 454 continue; 455 } 456 457 if ($char === "|") { 458 $simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset)); 459 460 $simpleTypes[] = SimpleType::fromString($simpleTypeName); 461 462 $simpleTypeOffset = $i + 1; 463 } 464 } 465 466 return new Type($simpleTypes); 467 } 468 469 /** 470 * @param SimpleType[] $types 471 */ 472 private function __construct(array $types) { 473 $this->types = $types; 474 } 475 476 public function isScalar(): bool { 477 foreach ($this->types as $type) { 478 if (!$type->isScalar()) { 479 return false; 480 } 481 } 482 483 return true; 484 } 485 486 public function isNullable(): bool { 487 foreach ($this->types as $type) { 488 if ($type->isNull()) { 489 return true; 490 } 491 } 492 493 return false; 494 } 495 496 public function getWithoutNull(): Type { 497 return new Type( 498 array_filter( 499 $this->types, 500 function(SimpleType $type) { 501 return !$type->isNull(); 502 } 503 ) 504 ); 505 } 506 507 public function tryToSimpleType(): ?SimpleType { 508 $withoutNull = $this->getWithoutNull(); 509 if (count($withoutNull->types) === 1) { 510 return $withoutNull->types[0]; 511 } 512 return null; 513 } 514 515 public function toArginfoType(): ArginfoType { 516 $classTypes = []; 517 $builtinTypes = []; 518 foreach ($this->types as $type) { 519 if ($type->isBuiltin) { 520 $builtinTypes[] = $type; 521 } else { 522 $classTypes[] = $type; 523 } 524 } 525 return new ArginfoType($classTypes, $builtinTypes); 526 } 527 528 public function toOptimizerTypeMask(): string { 529 $optimizerTypes = []; 530 531 foreach ($this->types as $type) { 532 $optimizerTypes[] = $type->toOptimizerTypeMask(); 533 } 534 535 return implode("|", $optimizerTypes); 536 } 537 538 public function toOptimizerTypeMaskForArrayKey(): string { 539 $typeMasks = []; 540 541 foreach ($this->types as $type) { 542 $typeMasks[] = $type->toOptimizerTypeMaskForArrayKey(); 543 } 544 545 return implode("|", $typeMasks); 546 } 547 548 public function toOptimizerTypeMaskForArrayValue(): string { 549 $typeMasks = []; 550 551 foreach ($this->types as $type) { 552 $typeMasks[] = $type->toOptimizerTypeMaskForArrayValue(); 553 } 554 555 return implode("|", $typeMasks); 556 } 557 558 public function getTypeForDoc(DOMDocument $doc): DOMElement { 559 if (count($this->types) > 1) { 560 $typeElement = $doc->createElement('type'); 561 $typeElement->setAttribute("class", "union"); 562 563 foreach ($this->types as $type) { 564 $unionTypeElement = $doc->createElement('type', $type->name); 565 $typeElement->appendChild($unionTypeElement); 566 } 567 } else { 568 $type = $this->types[0]; 569 if ($type->isBuiltin && strtolower($type->name) === "true") { 570 $name = "bool"; 571 } else { 572 $name = $type->name; 573 } 574 575 $typeElement = $doc->createElement('type', $name); 576 } 577 578 return $typeElement; 579 } 580 581 public static function equals(?Type $a, ?Type $b): bool { 582 if ($a === null || $b === null) { 583 return $a === $b; 584 } 585 586 if (count($a->types) !== count($b->types)) { 587 return false; 588 } 589 590 for ($i = 0; $i < count($a->types); $i++) { 591 if (!$a->types[$i]->equals($b->types[$i])) { 592 return false; 593 } 594 } 595 596 return true; 597 } 598 599 public function __toString() { 600 if ($this->types === null) { 601 return 'mixed'; 602 } 603 604 return implode('|', array_map( 605 function ($type) { return $type->name; }, 606 $this->types) 607 ); 608 } 609} 610 611class ArginfoType { 612 /** @var SimpleType[] $classTypes */ 613 public $classTypes; 614 615 /** @var SimpleType[] $builtinTypes */ 616 private $builtinTypes; 617 618 /** 619 * @param SimpleType[] $classTypes 620 * @param SimpleType[] $builtinTypes 621 */ 622 public function __construct(array $classTypes, array $builtinTypes) { 623 $this->classTypes = $classTypes; 624 $this->builtinTypes = $builtinTypes; 625 } 626 627 public function hasClassType(): bool { 628 return !empty($this->classTypes); 629 } 630 631 public function toClassTypeString(): string { 632 return implode('|', array_map(function(SimpleType $type) { 633 return $type->toEscapedName(); 634 }, $this->classTypes)); 635 } 636 637 public function toTypeMask(): string { 638 if (empty($this->builtinTypes)) { 639 return '0'; 640 } 641 return implode('|', array_map(function(SimpleType $type) { 642 return $type->toTypeMask(); 643 }, $this->builtinTypes)); 644 } 645} 646 647class ArgInfo { 648 const SEND_BY_VAL = 0; 649 const SEND_BY_REF = 1; 650 const SEND_PREFER_REF = 2; 651 652 /** @var string */ 653 public $name; 654 /** @var int */ 655 public $sendBy; 656 /** @var bool */ 657 public $isVariadic; 658 /** @var Type|null */ 659 public $type; 660 /** @var Type|null */ 661 public $phpDocType; 662 /** @var string|null */ 663 public $defaultValue; 664 665 public function __construct(string $name, int $sendBy, bool $isVariadic, ?Type $type, ?Type $phpDocType, ?string $defaultValue) { 666 $this->name = $name; 667 $this->sendBy = $sendBy; 668 $this->isVariadic = $isVariadic; 669 $this->setTypes($type, $phpDocType); 670 $this->defaultValue = $defaultValue; 671 } 672 673 public function equals(ArgInfo $other): bool { 674 return $this->name === $other->name 675 && $this->sendBy === $other->sendBy 676 && $this->isVariadic === $other->isVariadic 677 && Type::equals($this->type, $other->type) 678 && $this->defaultValue === $other->defaultValue; 679 } 680 681 public function getSendByString(): string { 682 switch ($this->sendBy) { 683 case self::SEND_BY_VAL: 684 return "0"; 685 case self::SEND_BY_REF: 686 return "1"; 687 case self::SEND_PREFER_REF: 688 return "ZEND_SEND_PREFER_REF"; 689 } 690 throw new Exception("Invalid sendBy value"); 691 } 692 693 public function getMethodSynopsisType(): Type { 694 if ($this->type) { 695 return $this->type; 696 } 697 698 if ($this->phpDocType) { 699 return $this->phpDocType; 700 } 701 702 throw new Exception("A parameter must have a type"); 703 } 704 705 public function hasProperDefaultValue(): bool { 706 return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN"; 707 } 708 709 public function getDefaultValueAsArginfoString(): string { 710 if ($this->hasProperDefaultValue()) { 711 return '"' . addslashes($this->defaultValue) . '"'; 712 } 713 714 return "NULL"; 715 } 716 717 public function getDefaultValueAsMethodSynopsisString(): ?string { 718 if ($this->defaultValue === null) { 719 return null; 720 } 721 722 switch ($this->defaultValue) { 723 case 'UNKNOWN': 724 return null; 725 case 'false': 726 case 'true': 727 case 'null': 728 return "&{$this->defaultValue};"; 729 } 730 731 return $this->defaultValue; 732 } 733 734 private function setTypes(?Type $type, ?Type $phpDocType): void 735 { 736 if ($phpDocType !== null && Type::equals($type, $phpDocType)) { 737 throw new Exception('PHPDoc param type "' . $phpDocType->__toString() . '" is unnecessary'); 738 } 739 740 $this->type = $type; 741 $this->phpDocType = $phpDocType; 742 } 743} 744 745class PropertyName { 746 /** @var Name */ 747 public $class; 748 /** @var string */ 749 public $property; 750 751 public function __construct(Name $class, string $property) 752 { 753 $this->class = $class; 754 $this->property = $property; 755 } 756 757 public function __toString() 758 { 759 return $this->class->toString() . "::$" . $this->property; 760 } 761} 762 763interface FunctionOrMethodName { 764 public function getDeclaration(): string; 765 public function getArgInfoName(): string; 766 public function getMethodSynopsisFilename(): string; 767 public function __toString(): string; 768 public function isMethod(): bool; 769 public function isConstructor(): bool; 770 public function isDestructor(): bool; 771} 772 773class FunctionName implements FunctionOrMethodName { 774 /** @var Name */ 775 private $name; 776 777 public function __construct(Name $name) { 778 $this->name = $name; 779 } 780 781 public function getNamespace(): ?string { 782 if ($this->name->isQualified()) { 783 return $this->name->slice(0, -1)->toString(); 784 } 785 return null; 786 } 787 788 public function getNonNamespacedName(): string { 789 if ($this->name->isQualified()) { 790 throw new Exception("Namespaced name not supported here"); 791 } 792 return $this->name->toString(); 793 } 794 795 public function getDeclarationName(): string { 796 return $this->name->getLast(); 797 } 798 799 public function getDeclaration(): string { 800 return "ZEND_FUNCTION({$this->getDeclarationName()});\n"; 801 } 802 803 public function getArgInfoName(): string { 804 $underscoreName = implode('_', $this->name->parts); 805 return "arginfo_$underscoreName"; 806 } 807 808 public function getMethodSynopsisFilename(): string { 809 return implode('_', $this->name->parts); 810 } 811 812 public function __toString(): string { 813 return $this->name->toString(); 814 } 815 816 public function isMethod(): bool { 817 return false; 818 } 819 820 public function isConstructor(): bool { 821 return false; 822 } 823 824 public function isDestructor(): bool { 825 return false; 826 } 827} 828 829class MethodName implements FunctionOrMethodName { 830 /** @var Name */ 831 private $className; 832 /** @var string */ 833 public $methodName; 834 835 public function __construct(Name $className, string $methodName) { 836 $this->className = $className; 837 $this->methodName = $methodName; 838 } 839 840 public function getDeclarationClassName(): string { 841 return implode('_', $this->className->parts); 842 } 843 844 public function getDeclaration(): string { 845 return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; 846 } 847 848 public function getArgInfoName(): string { 849 return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; 850 } 851 852 public function getMethodSynopsisFilename(): string { 853 return $this->getDeclarationClassName() . "_{$this->methodName}"; 854 } 855 856 public function __toString(): string { 857 return "$this->className::$this->methodName"; 858 } 859 860 public function isMethod(): bool { 861 return true; 862 } 863 864 public function isConstructor(): bool { 865 return $this->methodName === "__construct"; 866 } 867 868 public function isDestructor(): bool { 869 return $this->methodName === "__destruct"; 870 } 871} 872 873class ReturnInfo { 874 const REFCOUNT_0 = "0"; 875 const REFCOUNT_1 = "1"; 876 const REFCOUNT_N = "N"; 877 878 const REFCOUNTS = [ 879 self::REFCOUNT_0, 880 self::REFCOUNT_1, 881 self::REFCOUNT_N, 882 ]; 883 884 /** @var bool */ 885 public $byRef; 886 /** @var Type|null */ 887 public $type; 888 /** @var Type|null */ 889 public $phpDocType; 890 /** @var bool */ 891 public $tentativeReturnType; 892 /** @var string */ 893 public $refcount; 894 895 public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType, bool $tentativeReturnType, ?string $refcount) { 896 $this->byRef = $byRef; 897 $this->setTypes($type, $phpDocType, $tentativeReturnType); 898 $this->setRefcount($refcount); 899 } 900 901 public function equalsApartFromPhpDocAndRefcount(ReturnInfo $other): bool { 902 return $this->byRef === $other->byRef 903 && Type::equals($this->type, $other->type) 904 && $this->tentativeReturnType === $other->tentativeReturnType; 905 } 906 907 public function getMethodSynopsisType(): ?Type { 908 return $this->type ?? $this->phpDocType; 909 } 910 911 private function setTypes(?Type $type, ?Type $phpDocType, bool $tentativeReturnType): void 912 { 913 if ($phpDocType !== null && Type::equals($type, $phpDocType)) { 914 throw new Exception('PHPDoc return type "' . $phpDocType->__toString() . '" is unnecessary'); 915 } 916 917 $this->type = $type; 918 $this->phpDocType = $phpDocType; 919 $this->tentativeReturnType = $tentativeReturnType; 920 } 921 922 private function setRefcount(?string $refcount): void 923 { 924 $type = $this->phpDocType ?? $this->type; 925 $isScalarType = $type !== null && $type->isScalar(); 926 927 if ($refcount === null) { 928 $this->refcount = $isScalarType ? self::REFCOUNT_0 : self::REFCOUNT_N; 929 return; 930 } 931 932 if (!in_array($refcount, ReturnInfo::REFCOUNTS, true)) { 933 throw new Exception("@refcount must have one of the following values: \"0\", \"1\", \"N\", $refcount given"); 934 } 935 936 if ($isScalarType && $refcount !== self::REFCOUNT_0) { 937 throw new Exception('A scalar return type of "' . $type->__toString() . '" must have a refcount of "' . self::REFCOUNT_0 . '"'); 938 } 939 940 if (!$isScalarType && $refcount === self::REFCOUNT_0) { 941 throw new Exception('A non-scalar return type of "' . $type->__toString() . '" cannot have a refcount of "' . self::REFCOUNT_0 . '"'); 942 } 943 944 $this->refcount = $refcount; 945 } 946} 947 948class FuncInfo { 949 /** @var FunctionOrMethodName */ 950 public $name; 951 /** @var int */ 952 public $classFlags; 953 /** @var int */ 954 public $flags; 955 /** @var string|null */ 956 public $aliasType; 957 /** @var FunctionName|null */ 958 public $alias; 959 /** @var bool */ 960 public $isDeprecated; 961 /** @var bool */ 962 public $verify; 963 /** @var ArgInfo[] */ 964 public $args; 965 /** @var ReturnInfo */ 966 public $return; 967 /** @var int */ 968 public $numRequiredArgs; 969 /** @var string|null */ 970 public $cond; 971 972 /** 973 * @param ArgInfo[] $args 974 */ 975 public function __construct( 976 FunctionOrMethodName $name, 977 int $classFlags, 978 int $flags, 979 ?string $aliasType, 980 ?FunctionOrMethodName $alias, 981 bool $isDeprecated, 982 bool $verify, 983 array $args, 984 ReturnInfo $return, 985 int $numRequiredArgs, 986 ?string $cond 987 ) { 988 $this->name = $name; 989 $this->classFlags = $classFlags; 990 $this->flags = $flags; 991 $this->aliasType = $aliasType; 992 $this->alias = $alias; 993 $this->isDeprecated = $isDeprecated; 994 $this->verify = $verify; 995 $this->args = $args; 996 $this->return = $return; 997 $this->numRequiredArgs = $numRequiredArgs; 998 $this->cond = $cond; 999 } 1000 1001 public function isMethod(): bool 1002 { 1003 return $this->name->isMethod(); 1004 } 1005 1006 public function isFinalMethod(): bool 1007 { 1008 return ($this->flags & Class_::MODIFIER_FINAL) || ($this->classFlags & Class_::MODIFIER_FINAL); 1009 } 1010 1011 public function isInstanceMethod(): bool 1012 { 1013 return !($this->flags & Class_::MODIFIER_STATIC) && $this->isMethod() && !$this->name->isConstructor(); 1014 } 1015 1016 /** @return string[] */ 1017 public function getModifierNames(): array 1018 { 1019 if (!$this->isMethod()) { 1020 return []; 1021 } 1022 1023 $result = []; 1024 1025 if ($this->flags & Class_::MODIFIER_FINAL) { 1026 $result[] = "final"; 1027 } elseif ($this->flags & Class_::MODIFIER_ABSTRACT && $this->classFlags & ~Class_::MODIFIER_ABSTRACT) { 1028 $result[] = "abstract"; 1029 } 1030 1031 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1032 $result[] = "protected"; 1033 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1034 $result[] = "private"; 1035 } else { 1036 $result[] = "public"; 1037 } 1038 1039 if ($this->flags & Class_::MODIFIER_STATIC) { 1040 $result[] = "static"; 1041 } 1042 1043 return $result; 1044 } 1045 1046 public function hasParamWithUnknownDefaultValue(): bool 1047 { 1048 foreach ($this->args as $arg) { 1049 if ($arg->defaultValue && !$arg->hasProperDefaultValue()) { 1050 return true; 1051 } 1052 } 1053 1054 return false; 1055 } 1056 1057 public function equalsApartFromNameAndRefcount(FuncInfo $other): bool { 1058 if (count($this->args) !== count($other->args)) { 1059 return false; 1060 } 1061 1062 for ($i = 0; $i < count($this->args); $i++) { 1063 if (!$this->args[$i]->equals($other->args[$i])) { 1064 return false; 1065 } 1066 } 1067 1068 return $this->return->equalsApartFromPhpDocAndRefcount($other->return) 1069 && $this->numRequiredArgs === $other->numRequiredArgs 1070 && $this->cond === $other->cond; 1071 } 1072 1073 public function getArgInfoName(): string { 1074 return $this->name->getArgInfoName(); 1075 } 1076 1077 public function getDeclarationKey(): string 1078 { 1079 $name = $this->alias ?? $this->name; 1080 1081 return "$name|$this->cond"; 1082 } 1083 1084 public function getDeclaration(): ?string 1085 { 1086 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1087 return null; 1088 } 1089 1090 $name = $this->alias ?? $this->name; 1091 1092 return $name->getDeclaration(); 1093 } 1094 1095 public function getFunctionEntry(): string { 1096 if ($this->name instanceof MethodName) { 1097 if ($this->alias) { 1098 if ($this->alias instanceof MethodName) { 1099 return sprintf( 1100 "\tZEND_MALIAS(%s, %s, %s, %s, %s)\n", 1101 $this->alias->getDeclarationClassName(), $this->name->methodName, 1102 $this->alias->methodName, $this->getArgInfoName(), $this->getFlagsAsArginfoString() 1103 ); 1104 } else if ($this->alias instanceof FunctionName) { 1105 return sprintf( 1106 "\tZEND_ME_MAPPING(%s, %s, %s, %s)\n", 1107 $this->name->methodName, $this->alias->getNonNamespacedName(), 1108 $this->getArgInfoName(), $this->getFlagsAsArginfoString() 1109 ); 1110 } else { 1111 throw new Error("Cannot happen"); 1112 } 1113 } else { 1114 $declarationClassName = $this->name->getDeclarationClassName(); 1115 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1116 return sprintf( 1117 "\tZEND_ABSTRACT_ME_WITH_FLAGS(%s, %s, %s, %s)\n", 1118 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 1119 $this->getFlagsAsArginfoString() 1120 ); 1121 } 1122 1123 return sprintf( 1124 "\tZEND_ME(%s, %s, %s, %s)\n", 1125 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 1126 $this->getFlagsAsArginfoString() 1127 ); 1128 } 1129 } else if ($this->name instanceof FunctionName) { 1130 $namespace = $this->name->getNamespace(); 1131 $declarationName = $this->name->getDeclarationName(); 1132 1133 if ($this->alias && $this->isDeprecated) { 1134 return sprintf( 1135 "\tZEND_DEP_FALIAS(%s, %s, %s)\n", 1136 $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() 1137 ); 1138 } 1139 1140 if ($this->alias) { 1141 return sprintf( 1142 "\tZEND_FALIAS(%s, %s, %s)\n", 1143 $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() 1144 ); 1145 } 1146 1147 if ($this->isDeprecated) { 1148 return sprintf( 1149 "\tZEND_DEP_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); 1150 } 1151 1152 if ($namespace) { 1153 // Render A\B as "A\\B" in C strings for namespaces 1154 return sprintf( 1155 "\tZEND_NS_FE(\"%s\", %s, %s)\n", 1156 addslashes($namespace), $declarationName, $this->getArgInfoName()); 1157 } else { 1158 return sprintf("\tZEND_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); 1159 } 1160 } else { 1161 throw new Error("Cannot happen"); 1162 } 1163 } 1164 1165 public function getOptimizerInfo(): ?string { 1166 if ($this->isMethod()) { 1167 return null; 1168 } 1169 1170 if ($this->alias !== null) { 1171 return null; 1172 } 1173 1174 if ($this->return->refcount !== ReturnInfo::REFCOUNT_1 && $this->return->phpDocType === null) { 1175 return null; 1176 } 1177 1178 $type = $this->return->phpDocType ?? $this->return->type; 1179 if ($type === null) { 1180 return null; 1181 } 1182 1183 return " F" . $this->return->refcount . '("' . $this->name->__toString() . '", ' . $type->toOptimizerTypeMask() . "),\n"; 1184 } 1185 1186 public function discardInfoForOldPhpVersions(): void { 1187 $this->return->type = null; 1188 foreach ($this->args as $arg) { 1189 $arg->type = null; 1190 $arg->defaultValue = null; 1191 } 1192 } 1193 1194 private function getFlagsAsArginfoString(): string 1195 { 1196 $flags = "ZEND_ACC_PUBLIC"; 1197 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1198 $flags = "ZEND_ACC_PROTECTED"; 1199 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1200 $flags = "ZEND_ACC_PRIVATE"; 1201 } 1202 1203 if ($this->flags & Class_::MODIFIER_STATIC) { 1204 $flags .= "|ZEND_ACC_STATIC"; 1205 } 1206 1207 if ($this->flags & Class_::MODIFIER_FINAL) { 1208 $flags .= "|ZEND_ACC_FINAL"; 1209 } 1210 1211 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1212 $flags .= "|ZEND_ACC_ABSTRACT"; 1213 } 1214 1215 if ($this->isDeprecated) { 1216 $flags .= "|ZEND_ACC_DEPRECATED"; 1217 } 1218 1219 return $flags; 1220 } 1221 1222 /** 1223 * @param array<string, FuncInfo> $funcMap 1224 * @param array<string, FuncInfo> $aliasMap 1225 * @throws Exception 1226 */ 1227 public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string { 1228 1229 $doc = new DOMDocument(); 1230 $doc->formatOutput = true; 1231 $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 1232 if (!$methodSynopsis) { 1233 return null; 1234 } 1235 1236 $doc->appendChild($methodSynopsis); 1237 1238 return $doc->saveXML(); 1239 } 1240 1241 /** 1242 * @param array<string, FuncInfo> $funcMap 1243 * @param array<string, FuncInfo> $aliasMap 1244 * @throws Exception 1245 */ 1246 public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement { 1247 if ($this->hasParamWithUnknownDefaultValue()) { 1248 return null; 1249 } 1250 1251 if ($this->name->isConstructor()) { 1252 $synopsisType = "constructorsynopsis"; 1253 } elseif ($this->name->isDestructor()) { 1254 $synopsisType = "destructorsynopsis"; 1255 } else { 1256 $synopsisType = "methodsynopsis"; 1257 } 1258 1259 $methodSynopsis = $doc->createElement($synopsisType); 1260 1261 $aliasedFunc = $this->aliasType === "alias" && isset($funcMap[$this->alias->__toString()]) ? $funcMap[$this->alias->__toString()] : null; 1262 $aliasFunc = $aliasMap[$this->name->__toString()] ?? null; 1263 1264 if (($this->aliasType === "alias" && $aliasedFunc !== null && $aliasedFunc->isMethod() !== $this->isMethod()) || 1265 ($aliasFunc !== null && $aliasFunc->isMethod() !== $this->isMethod()) 1266 ) { 1267 $role = $doc->createAttribute("role"); 1268 $role->value = $this->isMethod() ? "oop" : "procedural"; 1269 $methodSynopsis->appendChild($role); 1270 } 1271 1272 $methodSynopsis->appendChild(new DOMText("\n ")); 1273 1274 foreach ($this->getModifierNames() as $modifierString) { 1275 $modifierElement = $doc->createElement('modifier', $modifierString); 1276 $methodSynopsis->appendChild($modifierElement); 1277 $methodSynopsis->appendChild(new DOMText(" ")); 1278 } 1279 1280 $returnType = $this->return->getMethodSynopsisType(); 1281 if ($returnType) { 1282 $methodSynopsis->appendChild($returnType->getTypeForDoc($doc)); 1283 } 1284 1285 $methodname = $doc->createElement('methodname', $this->name->__toString()); 1286 $methodSynopsis->appendChild($methodname); 1287 1288 if (empty($this->args)) { 1289 $methodSynopsis->appendChild(new DOMText("\n ")); 1290 $void = $doc->createElement('void'); 1291 $methodSynopsis->appendChild($void); 1292 } else { 1293 foreach ($this->args as $arg) { 1294 $methodSynopsis->appendChild(new DOMText("\n ")); 1295 $methodparam = $doc->createElement('methodparam'); 1296 if ($arg->defaultValue !== null) { 1297 $methodparam->setAttribute("choice", "opt"); 1298 } 1299 if ($arg->isVariadic) { 1300 $methodparam->setAttribute("rep", "repeat"); 1301 } 1302 1303 $methodSynopsis->appendChild($methodparam); 1304 $methodparam->appendChild($arg->getMethodSynopsisType()->getTypeForDoc($doc)); 1305 1306 $parameter = $doc->createElement('parameter', $arg->name); 1307 if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) { 1308 $parameter->setAttribute("role", "reference"); 1309 } 1310 1311 $methodparam->appendChild($parameter); 1312 $defaultValue = $arg->getDefaultValueAsMethodSynopsisString(); 1313 if ($defaultValue !== null) { 1314 $initializer = $doc->createElement('initializer'); 1315 if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) { 1316 $constant = $doc->createElement('constant', $defaultValue); 1317 $initializer->appendChild($constant); 1318 } else { 1319 $initializer->nodeValue = $defaultValue; 1320 } 1321 $methodparam->appendChild($initializer); 1322 } 1323 } 1324 } 1325 $methodSynopsis->appendChild(new DOMText("\n ")); 1326 1327 return $methodSynopsis; 1328 } 1329 1330 public function __clone() 1331 { 1332 foreach ($this->args as $key => $argInfo) { 1333 $this->args[$key] = clone $argInfo; 1334 } 1335 $this->return = clone $this->return; 1336 } 1337} 1338 1339function initializeZval(string $zvalName, $value): string 1340{ 1341 $code = "\tzval $zvalName;\n"; 1342 1343 switch (gettype($value)) { 1344 case "NULL": 1345 $code .= "\tZVAL_NULL(&$zvalName);\n"; 1346 break; 1347 1348 case "boolean": 1349 $code .= "\tZVAL_BOOL(&$zvalName, " . ((int) $value) . ");\n"; 1350 break; 1351 1352 case "integer": 1353 $code .= "\tZVAL_LONG(&$zvalName, $value);\n"; 1354 break; 1355 1356 case "double": 1357 $code .= "\tZVAL_DOUBLE(&$zvalName, $value);\n"; 1358 break; 1359 1360 case "string": 1361 if ($value === "") { 1362 $code .= "\tZVAL_EMPTY_STRING(&$zvalName);\n"; 1363 } else { 1364 $strValue = addslashes($value); 1365 $code .= "\tzend_string *{$zvalName}_str = zend_string_init(\"$strValue\", sizeof(\"$strValue\") - 1, 1);\n"; 1366 $code .= "\tZVAL_STR(&$zvalName, {$zvalName}_str);\n"; 1367 } 1368 break; 1369 1370 case "array": 1371 if (empty($value)) { 1372 $code .= "\tZVAL_EMPTY_ARRAY(&$zvalName);\n"; 1373 } else { 1374 throw new Exception("Unimplemented default value"); 1375 } 1376 break; 1377 1378 default: 1379 throw new Exception("Invalid default value"); 1380 } 1381 1382 return $code; 1383} 1384 1385class PropertyInfo 1386{ 1387 /** @var PropertyName */ 1388 public $name; 1389 /** @var int */ 1390 public $flags; 1391 /** @var Type|null */ 1392 public $type; 1393 /** @var Type|null */ 1394 public $phpDocType; 1395 /** @var Expr|null */ 1396 public $defaultValue; 1397 /** @var string|null */ 1398 public $defaultValueString; 1399 /** @var bool */ 1400 public $isDocReadonly; 1401 /** @var string|null */ 1402 public $link; 1403 1404 public function __construct( 1405 PropertyName $name, 1406 int $flags, 1407 ?Type $type, 1408 ?Type $phpDocType, 1409 ?Expr $defaultValue, 1410 ?string $defaultValueString, 1411 bool $isDocReadonly, 1412 ?string $link 1413 ) { 1414 $this->name = $name; 1415 $this->flags = $flags; 1416 $this->type = $type; 1417 $this->phpDocType = $phpDocType; 1418 $this->defaultValue = $defaultValue; 1419 $this->defaultValueString = $defaultValueString; 1420 $this->isDocReadonly = $isDocReadonly; 1421 $this->link = $link; 1422 } 1423 1424 public function discardInfoForOldPhpVersions(): void { 1425 $this->type = null; 1426 } 1427 1428 public function getDeclaration(): string { 1429 $code = "\n"; 1430 1431 $propertyName = $this->name->property; 1432 1433 $defaultValueConstant = false; 1434 if ($this->defaultValue === null) { 1435 $defaultValue = null; 1436 } else { 1437 $defaultValue = $this->evaluateDefaultValue($defaultValueConstant); 1438 } 1439 1440 if ($defaultValueConstant) { 1441 echo "Skipping code generation for property $this->name, because it has a constant default value\n"; 1442 return ""; 1443 } 1444 1445 $typeCode = ""; 1446 if ($this->type) { 1447 $arginfoType = $this->type->toArginfoType(); 1448 if ($arginfoType->hasClassType()) { 1449 if (count($arginfoType->classTypes) >= 2) { 1450 foreach ($arginfoType->classTypes as $classType) { 1451 $escapedClassName = $classType->toEscapedName(); 1452 $varEscapedClassName = $classType->toVarEscapedName(); 1453 $code .= "\tzend_string *property_{$propertyName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\") - 1, 1);\n"; 1454 } 1455 1456 $classTypeCount = count($arginfoType->classTypes); 1457 $code .= "\tzend_type_list *property_{$propertyName}_type_list = malloc(ZEND_TYPE_LIST_SIZE($classTypeCount));\n"; 1458 $code .= "\tproperty_{$propertyName}_type_list->num_types = $classTypeCount;\n"; 1459 1460 foreach ($arginfoType->classTypes as $k => $classType) { 1461 $escapedClassName = $classType->toEscapedName(); 1462 $code .= "\tproperty_{$propertyName}_type_list->types[$k] = (zend_type) ZEND_TYPE_INIT_CLASS(property_{$propertyName}_class_{$escapedClassName}, 0, 0);\n"; 1463 } 1464 1465 $typeMaskCode = $this->type->toArginfoType()->toTypeMask(); 1466 1467 $code .= "\tzend_type property_{$propertyName}_type = ZEND_TYPE_INIT_UNION(property_{$propertyName}_type_list, $typeMaskCode);\n"; 1468 $typeCode = "property_{$propertyName}_type"; 1469 } else { 1470 $escapedClassName = $arginfoType->classTypes[0]->toEscapedName(); 1471 $varEscapedClassName = $arginfoType->classTypes[0]->toVarEscapedName(); 1472 $code .= "\tzend_string *property_{$propertyName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"${escapedClassName}\")-1, 1);\n"; 1473 1474 $typeCode = "(zend_type) ZEND_TYPE_INIT_CLASS(property_{$propertyName}_class_{$varEscapedClassName}, 0, " . $arginfoType->toTypeMask() . ")"; 1475 } 1476 } else { 1477 $typeCode = "(zend_type) ZEND_TYPE_INIT_MASK(" . $arginfoType->toTypeMask() . ")"; 1478 } 1479 } 1480 1481 $zvalName = "property_{$this->name->property}_default_value"; 1482 if ($this->defaultValue === null && $this->type !== null) { 1483 $code .= "\tzval $zvalName;\n\tZVAL_UNDEF(&$zvalName);\n"; 1484 } else { 1485 $code .= initializeZval($zvalName, $defaultValue); 1486 } 1487 1488 $code .= "\tzend_string *property_{$propertyName}_name = zend_string_init(\"$propertyName\", sizeof(\"$propertyName\") - 1, 1);\n"; 1489 $nameCode = "property_{$propertyName}_name"; 1490 1491 if ($this->type !== null) { 1492 $code .= "\tzend_declare_typed_property(class_entry, $nameCode, &$zvalName, " . $this->getFlagsAsString() . ", NULL, $typeCode);\n"; 1493 } else { 1494 $code .= "\tzend_declare_property_ex(class_entry, $nameCode, &$zvalName, " . $this->getFlagsAsString() . ", NULL);\n"; 1495 } 1496 $code .= "\tzend_string_release(property_{$propertyName}_name);\n"; 1497 1498 return $code; 1499 } 1500 1501 private function getFlagsAsString(): string 1502 { 1503 $flags = "ZEND_ACC_PUBLIC"; 1504 if ($this->flags & Class_::MODIFIER_PROTECTED) { 1505 $flags = "ZEND_ACC_PROTECTED"; 1506 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1507 $flags = "ZEND_ACC_PRIVATE"; 1508 } 1509 1510 if ($this->flags & Class_::MODIFIER_STATIC) { 1511 $flags .= "|ZEND_ACC_STATIC"; 1512 } 1513 1514 if ($this->flags & Class_::MODIFIER_READONLY) { 1515 $flags .= "|ZEND_ACC_READONLY"; 1516 } 1517 1518 return $flags; 1519 } 1520 1521 public function getFieldSynopsisElement(DOMDocument $doc): DOMElement 1522 { 1523 $fieldsynopsisElement = $doc->createElement("fieldsynopsis"); 1524 1525 if ($this->flags & Class_::MODIFIER_PUBLIC) { 1526 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1527 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "public")); 1528 } elseif ($this->flags & Class_::MODIFIER_PROTECTED) { 1529 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1530 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected")); 1531 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 1532 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1533 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private")); 1534 } 1535 1536 if ($this->flags & Class_::MODIFIER_STATIC) { 1537 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1538 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "static")); 1539 } elseif ($this->flags & Class_::MODIFIER_READONLY || $this->isDocReadonly) { 1540 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1541 $fieldsynopsisElement->appendChild($doc->createElement("modifier", "readonly")); 1542 } 1543 1544 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1545 $fieldsynopsisElement->appendChild($this->getFieldSynopsisType()->getTypeForDoc($doc)); 1546 1547 $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString()); 1548 $varnameElement = $doc->createElement("varname", $this->name->property); 1549 if ($this->link) { 1550 $varnameElement->setAttribute("linkend", $this->link); 1551 } else { 1552 $varnameElement->setAttribute("linkend", "$className.props." . strtolower(str_replace("_", "-", $this->name->property))); 1553 } 1554 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1555 $fieldsynopsisElement->appendChild($varnameElement); 1556 1557 if ($this->defaultValueString) { 1558 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1559 $initializerElement = $doc->createElement("initializer", $this->defaultValueString); 1560 $fieldsynopsisElement->appendChild($initializerElement); 1561 } 1562 1563 $fieldsynopsisElement->appendChild(new DOMText("\n ")); 1564 1565 return $fieldsynopsisElement; 1566 } 1567 1568 private function getFieldSynopsisType(): Type { 1569 if ($this->phpDocType) { 1570 return $this->phpDocType; 1571 } 1572 1573 if ($this->type) { 1574 return $this->type; 1575 } 1576 1577 throw new Exception("A property must have a type"); 1578 } 1579 1580 /** @return mixed */ 1581 private function evaluateDefaultValue(bool &$defaultValueConstant) 1582 { 1583 $evaluator = new ConstExprEvaluator( 1584 function (Expr $expr) use (&$defaultValueConstant) { 1585 if ($expr instanceof Expr\ConstFetch) { 1586 $defaultValueConstant = true; 1587 return null; 1588 } 1589 1590 throw new Exception("Property $this->name has an unsupported default value"); 1591 } 1592 ); 1593 1594 return $evaluator->evaluateDirectly($this->defaultValue); 1595 } 1596 1597 public function __clone() 1598 { 1599 if ($this->type) { 1600 $this->type = clone $this->type; 1601 } 1602 } 1603} 1604 1605class EnumCaseInfo { 1606 /** @var string */ 1607 public $name; 1608 /** @var Expr|null */ 1609 public $value; 1610 1611 public function __construct(string $name, ?Expr $value) { 1612 $this->name = $name; 1613 $this->value = $value; 1614 } 1615 1616 public function getDeclaration(): string { 1617 $escapedName = addslashes($this->name); 1618 if ($this->value === null) { 1619 $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n"; 1620 } else { 1621 $evaluator = new ConstExprEvaluator(function (Expr $expr) { 1622 throw new Exception("Enum case $this->name has an unsupported value"); 1623 }); 1624 $zvalName = "enum_case_{$escapedName}_value"; 1625 $code = "\n" . initializeZval($zvalName, $evaluator->evaluateDirectly($this->value)); 1626 $code .= "\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", &$zvalName);\n"; 1627 } 1628 return $code; 1629 } 1630} 1631 1632class ClassInfo { 1633 /** @var Name */ 1634 public $name; 1635 /** @var int */ 1636 public $flags; 1637 /** @var string */ 1638 public $type; 1639 /** @var string|null */ 1640 public $alias; 1641 /** @var SimpleType|null */ 1642 public $enumBackingType; 1643 /** @var bool */ 1644 public $isDeprecated; 1645 /** @var bool */ 1646 public $isStrictProperties; 1647 /** @var bool */ 1648 public $isNotSerializable; 1649 /** @var Name[] */ 1650 public $extends; 1651 /** @var Name[] */ 1652 public $implements; 1653 /** @var PropertyInfo[] */ 1654 public $propertyInfos; 1655 /** @var FuncInfo[] */ 1656 public $funcInfos; 1657 /** @var EnumCaseInfo[] */ 1658 public $enumCaseInfos; 1659 1660 /** 1661 * @param Name[] $extends 1662 * @param Name[] $implements 1663 * @param PropertyInfo[] $propertyInfos 1664 * @param FuncInfo[] $funcInfos 1665 * @param EnumCaseInfo[] $enumCaseInfos 1666 */ 1667 public function __construct( 1668 Name $name, 1669 int $flags, 1670 string $type, 1671 ?string $alias, 1672 ?SimpleType $enumBackingType, 1673 bool $isDeprecated, 1674 bool $isStrictProperties, 1675 bool $isNotSerializable, 1676 array $extends, 1677 array $implements, 1678 array $propertyInfos, 1679 array $funcInfos, 1680 array $enumCaseInfos 1681 ) { 1682 $this->name = $name; 1683 $this->flags = $flags; 1684 $this->type = $type; 1685 $this->alias = $alias; 1686 $this->enumBackingType = $enumBackingType; 1687 $this->isDeprecated = $isDeprecated; 1688 $this->isStrictProperties = $isStrictProperties; 1689 $this->isNotSerializable = $isNotSerializable; 1690 $this->extends = $extends; 1691 $this->implements = $implements; 1692 $this->propertyInfos = $propertyInfos; 1693 $this->funcInfos = $funcInfos; 1694 $this->enumCaseInfos = $enumCaseInfos; 1695 } 1696 1697 public function getRegistration(): string 1698 { 1699 $params = []; 1700 foreach ($this->extends as $extends) { 1701 $params[] = "zend_class_entry *class_entry_" . implode("_", $extends->parts); 1702 } 1703 foreach ($this->implements as $implements) { 1704 $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->parts); 1705 } 1706 1707 $escapedName = implode("_", $this->name->parts); 1708 1709 $code = "static zend_class_entry *register_class_$escapedName(" . (empty($params) ? "void" : implode(", ", $params)) . ")\n"; 1710 1711 $code .= "{\n"; 1712 if ($this->type == "enum") { 1713 $name = addslashes((string) $this->name); 1714 $backingType = $this->enumBackingType 1715 ? $this->enumBackingType->toTypeCode() : "IS_UNDEF"; 1716 $code .= "\tzend_class_entry *class_entry = zend_register_internal_enum(\"$name\", $backingType, class_{$escapedName}_methods);\n"; 1717 } else { 1718 $code .= "\tzend_class_entry ce, *class_entry;\n\n"; 1719 if (count($this->name->parts) > 1) { 1720 $className = $this->name->getLast(); 1721 $namespace = addslashes((string) $this->name->slice(0, -1)); 1722 1723 $code .= "\tINIT_NS_CLASS_ENTRY(ce, \"$namespace\", \"$className\", class_{$escapedName}_methods);\n"; 1724 } else { 1725 $code .= "\tINIT_CLASS_ENTRY(ce, \"$this->name\", class_{$escapedName}_methods);\n"; 1726 } 1727 1728 if ($this->type === "class" || $this->type === "trait") { 1729 $code .= "\tclass_entry = zend_register_internal_class_ex(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ");\n"; 1730 } else { 1731 $code .= "\tclass_entry = zend_register_internal_interface(&ce);\n"; 1732 } 1733 } 1734 1735 if ($this->getFlagsAsString()) { 1736 $code .= "\tclass_entry->ce_flags |= " . $this->getFlagsAsString() . ";\n"; 1737 } 1738 1739 $implements = array_map( 1740 function (Name $item) { 1741 return "class_entry_" . implode("_", $item->parts); 1742 }, 1743 $this->type === "interface" ? $this->extends : $this->implements 1744 ); 1745 1746 if (!empty($implements)) { 1747 $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n"; 1748 } 1749 1750 if ($this->alias) { 1751 $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n"; 1752 } 1753 1754 foreach ($this->enumCaseInfos as $enumCase) { 1755 $code .= $enumCase->getDeclaration(); 1756 } 1757 1758 foreach ($this->propertyInfos as $property) { 1759 $code .= $property->getDeclaration(); 1760 } 1761 1762 $code .= "\n\treturn class_entry;\n"; 1763 1764 $code .= "}\n"; 1765 1766 return $code; 1767 } 1768 1769 private function getFlagsAsString(): string 1770 { 1771 $flags = []; 1772 1773 if ($this->type === "trait") { 1774 $flags[] = "ZEND_ACC_TRAIT"; 1775 } 1776 1777 if ($this->flags & Class_::MODIFIER_FINAL) { 1778 $flags[] = "ZEND_ACC_FINAL"; 1779 } 1780 1781 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 1782 $flags[] = "ZEND_ACC_ABSTRACT"; 1783 } 1784 1785 if ($this->isDeprecated) { 1786 $flags[] = "ZEND_ACC_DEPRECATED"; 1787 } 1788 1789 if ($this->isStrictProperties) { 1790 $flags[] = "ZEND_ACC_NO_DYNAMIC_PROPERTIES"; 1791 } 1792 1793 if ($this->isNotSerializable) { 1794 $flags[] = "ZEND_ACC_NOT_SERIALIZABLE"; 1795 } 1796 1797 return implode("|", $flags); 1798 } 1799 1800 /** 1801 * @param array<string, ClassInfo> $classMap 1802 */ 1803 public function getClassSynopsisDocument(array $classMap): ?string { 1804 1805 $doc = new DOMDocument(); 1806 $doc->formatOutput = true; 1807 $classSynopsis = $this->getClassSynopsisElement($doc, $classMap); 1808 if (!$classSynopsis) { 1809 return null; 1810 } 1811 1812 $doc->appendChild($classSynopsis); 1813 1814 return $doc->saveXML(); 1815 } 1816 1817 /** 1818 * @param ClassInfo[] $classMap 1819 */ 1820 public function getClassSynopsisElement(DOMDocument $doc, array $classMap): ?DOMElement { 1821 1822 $classSynopsis = $doc->createElement("classsynopsis"); 1823 $classSynopsis->appendChild(new DOMText("\n ")); 1824 1825 $ooElement = self::createOoElement($doc, $this, true, false, false, 4); 1826 if (!$ooElement) { 1827 return null; 1828 } 1829 $classSynopsis->appendChild($ooElement); 1830 $classSynopsis->appendChild(new DOMText("\n\n ")); 1831 1832 $classSynopsisInfo = $doc->createElement("classsynopsisinfo"); 1833 $classSynopsisInfo->appendChild(new DOMText("\n ")); 1834 $ooElement = self::createOoElement($doc, $this, false, true, false, 5); 1835 if (!$ooElement) { 1836 return null; 1837 } 1838 $classSynopsisInfo->appendChild($ooElement); 1839 1840 $classSynopsis->appendChild($classSynopsisInfo); 1841 1842 foreach ($this->extends as $k => $parent) { 1843 $parentInfo = $classMap[$parent->toString()] ?? null; 1844 if ($parentInfo === null) { 1845 throw new Exception("Missing parent class " . $parent->toString()); 1846 } 1847 1848 $ooElement = self::createOoElement( 1849 $doc, 1850 $parentInfo, 1851 $this->type === "interface", 1852 false, 1853 $k === 0, 1854 5 1855 ); 1856 if (!$ooElement) { 1857 return null; 1858 } 1859 1860 $classSynopsisInfo->appendChild(new DOMText("\n\n ")); 1861 $classSynopsisInfo->appendChild($ooElement); 1862 } 1863 1864 foreach ($this->implements as $interface) { 1865 $interfaceInfo = $classMap[$interface->toString()] ?? null; 1866 if (!$interfaceInfo) { 1867 throw new Exception("Missing implemented interface " . $interface->toString()); 1868 } 1869 1870 $ooElement = self::createOoElement($doc, $interfaceInfo, false, false, false, 5); 1871 if (!$ooElement) { 1872 return null; 1873 } 1874 $classSynopsisInfo->appendChild(new DOMText("\n\n ")); 1875 $classSynopsisInfo->appendChild($ooElement); 1876 } 1877 $classSynopsisInfo->appendChild(new DOMText("\n ")); 1878 1879 /** @var Name[] $parentsWithInheritedProperties */ 1880 $parentsWithInheritedProperties = []; 1881 /** @var Name[] $parentsWithInheritedMethods */ 1882 $parentsWithInheritedMethods = []; 1883 1884 $this->collectInheritedMembers($parentsWithInheritedProperties, $parentsWithInheritedMethods, $classMap); 1885 1886 if (!empty($this->propertyInfos)) { 1887 $classSynopsis->appendChild(new DOMText("\n\n ")); 1888 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;"); 1889 $classSynopsisInfo->setAttribute("role", "comment"); 1890 $classSynopsis->appendChild($classSynopsisInfo); 1891 1892 foreach ($this->propertyInfos as $propertyInfo) { 1893 $classSynopsis->appendChild(new DOMText("\n ")); 1894 $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc); 1895 $classSynopsis->appendChild($fieldSynopsisElement); 1896 } 1897 } 1898 1899 if (!empty($parentsWithInheritedProperties)) { 1900 $classSynopsis->appendChild(new DOMText("\n\n ")); 1901 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&InheritedProperties;"); 1902 $classSynopsisInfo->setAttribute("role", "comment"); 1903 $classSynopsis->appendChild($classSynopsisInfo); 1904 1905 foreach ($parentsWithInheritedProperties as $parent) { 1906 $classSynopsis->appendChild(new DOMText("\n ")); 1907 $parentReference = self::getClassSynopsisReference($parent); 1908 1909 $includeElement = $this->createIncludeElement( 1910 $doc, 1911 "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()='&Properties;']]))" 1912 ); 1913 $classSynopsis->appendChild($includeElement); 1914 } 1915 } 1916 1917 if (!empty($this->funcInfos)) { 1918 $classSynopsis->appendChild(new DOMText("\n\n ")); 1919 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Methods;"); 1920 $classSynopsisInfo->setAttribute("role", "comment"); 1921 $classSynopsis->appendChild($classSynopsisInfo); 1922 1923 $classReference = self::getClassSynopsisReference($this->name); 1924 1925 if ($this->hasConstructor()) { 1926 $classSynopsis->appendChild(new DOMText("\n ")); 1927 $includeElement = $this->createIncludeElement( 1928 $doc, 1929 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[not(@role='procedural')])" 1930 ); 1931 $classSynopsis->appendChild($includeElement); 1932 } 1933 1934 if ($this->hasMethods()) { 1935 $classSynopsis->appendChild(new DOMText("\n ")); 1936 $includeElement = $this->createIncludeElement( 1937 $doc, 1938 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[not(@role='procedural')])" 1939 ); 1940 $classSynopsis->appendChild($includeElement); 1941 } 1942 1943 if ($this->hasDestructor()) { 1944 $classSynopsis->appendChild(new DOMText("\n ")); 1945 $includeElement = $this->createIncludeElement( 1946 $doc, 1947 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[not(@role='procedural')])" 1948 ); 1949 $classSynopsis->appendChild($includeElement); 1950 } 1951 } 1952 1953 if (!empty($parentsWithInheritedMethods)) { 1954 $classSynopsis->appendChild(new DOMText("\n\n ")); 1955 $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&InheritedMethods;"); 1956 $classSynopsisInfo->setAttribute("role", "comment"); 1957 $classSynopsis->appendChild($classSynopsisInfo); 1958 1959 foreach ($parentsWithInheritedMethods as $parent) { 1960 $classSynopsis->appendChild(new DOMText("\n ")); 1961 $parentReference = self::getClassSynopsisReference($parent); 1962 $includeElement = $this->createIncludeElement( 1963 $doc, 1964 "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$parentReference')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[not(@role='procedural')])" 1965 ); 1966 $classSynopsis->appendChild($includeElement); 1967 } 1968 } 1969 1970 $classSynopsis->appendChild(new DOMText("\n ")); 1971 1972 return $classSynopsis; 1973 } 1974 1975 private static function createOoElement( 1976 DOMDocument $doc, 1977 ClassInfo $classInfo, 1978 bool $overrideToClass, 1979 bool $withModifiers, 1980 bool $isExtends, 1981 int $indentationLevel 1982 ): ?DOMElement { 1983 $indentation = str_repeat(" ", $indentationLevel); 1984 1985 if ($classInfo->type !== "class" && $classInfo->type !== "interface") { 1986 echo "Class synopsis generation is not implemented for " . $classInfo->type . "\n"; 1987 return null; 1988 } 1989 1990 $type = $overrideToClass ? "class" : $classInfo->type; 1991 1992 $ooElement = $doc->createElement("oo$type"); 1993 $ooElement->appendChild(new DOMText("\n$indentation ")); 1994 if ($isExtends) { 1995 $ooElement->appendChild($doc->createElement('modifier', 'extends')); 1996 $ooElement->appendChild(new DOMText("\n$indentation ")); 1997 } elseif ($withModifiers) { 1998 if ($classInfo->flags & Class_::MODIFIER_FINAL) { 1999 $ooElement->appendChild($doc->createElement('modifier', 'final')); 2000 $ooElement->appendChild(new DOMText("\n$indentation ")); 2001 } 2002 if ($classInfo->flags & Class_::MODIFIER_ABSTRACT) { 2003 $ooElement->appendChild($doc->createElement('modifier', 'abstract')); 2004 $ooElement->appendChild(new DOMText("\n$indentation ")); 2005 } 2006 } 2007 2008 $nameElement = $doc->createElement("{$type}name", $classInfo->name->toString()); 2009 $ooElement->appendChild($nameElement); 2010 $ooElement->appendChild(new DOMText("\n$indentation")); 2011 2012 return $ooElement; 2013 } 2014 2015 public static function getClassSynopsisFilename(Name $name): string { 2016 return strtolower(str_replace("_", "-", implode('-', $name->parts))); 2017 } 2018 2019 public static function getClassSynopsisReference(Name $name): string { 2020 return "class." . self::getClassSynopsisFilename($name); 2021 } 2022 2023 /** 2024 * @param Name[] $parentsWithInheritedProperties 2025 * @param Name[] $parentsWithInheritedMethods 2026 * @param array<string, ClassInfo> $classMap 2027 */ 2028 private function collectInheritedMembers(array &$parentsWithInheritedProperties, array &$parentsWithInheritedMethods, array $classMap): void 2029 { 2030 foreach ($this->extends as $parent) { 2031 $parentInfo = $classMap[$parent->toString()] ?? null; 2032 if (!$parentInfo) { 2033 throw new Exception("Missing parent class " . $parent->toString()); 2034 } 2035 2036 if (!empty($parentInfo->propertyInfos) && !isset($parentsWithInheritedProperties[$parent->toString()])) { 2037 $parentsWithInheritedProperties[$parent->toString()] = $parent; 2038 } 2039 2040 if (!isset($parentsWithInheritedMethods[$parent->toString()]) && $parentInfo->hasMethods()) { 2041 $parentsWithInheritedMethods[$parent->toString()] = $parent; 2042 } 2043 2044 $parentInfo->collectInheritedMembers($parentsWithInheritedProperties, $parentsWithInheritedMethods, $classMap); 2045 } 2046 } 2047 2048 private function hasConstructor(): bool 2049 { 2050 foreach ($this->funcInfos as $funcInfo) { 2051 if ($funcInfo->name->isConstructor()) { 2052 return true; 2053 } 2054 } 2055 2056 return false; 2057 } 2058 2059 private function hasDestructor(): bool 2060 { 2061 foreach ($this->funcInfos as $funcInfo) { 2062 if ($funcInfo->name->isDestructor()) { 2063 return true; 2064 } 2065 } 2066 2067 return false; 2068 } 2069 2070 private function hasMethods(): bool 2071 { 2072 foreach ($this->funcInfos as $funcInfo) { 2073 if (!$funcInfo->name->isConstructor() && !$funcInfo->name->isDestructor()) { 2074 return true; 2075 } 2076 } 2077 2078 return false; 2079 } 2080 2081 private function createIncludeElement(DOMDocument $doc, string $query): DOMElement 2082 { 2083 $includeElement = $doc->createElement("xi:include"); 2084 $attr = $doc->createAttribute("xpointer"); 2085 $attr->value = $query; 2086 $includeElement->appendChild($attr); 2087 $fallbackElement = $doc->createElement("xi:fallback"); 2088 $includeElement->appendChild(new DOMText("\n ")); 2089 $includeElement->appendChild($fallbackElement); 2090 $includeElement->appendChild(new DOMText("\n ")); 2091 2092 return $includeElement; 2093 } 2094 2095 public function __clone() 2096 { 2097 foreach ($this->propertyInfos as $key => $propertyInfo) { 2098 $this->propertyInfos[$key] = clone $propertyInfo; 2099 } 2100 2101 foreach ($this->funcInfos as $key => $funcInfo) { 2102 $this->funcInfos[$key] = clone $funcInfo; 2103 } 2104 } 2105} 2106 2107class FileInfo { 2108 /** @var FuncInfo[] */ 2109 public $funcInfos = []; 2110 /** @var ClassInfo[] */ 2111 public $classInfos = []; 2112 /** @var bool */ 2113 public $generateFunctionEntries = false; 2114 /** @var string */ 2115 public $declarationPrefix = ""; 2116 /** @var bool */ 2117 public $generateLegacyArginfo = false; 2118 /** @var bool */ 2119 public $generateClassEntries = false; 2120 2121 /** 2122 * @return iterable<FuncInfo> 2123 */ 2124 public function getAllFuncInfos(): iterable { 2125 yield from $this->funcInfos; 2126 foreach ($this->classInfos as $classInfo) { 2127 yield from $classInfo->funcInfos; 2128 } 2129 } 2130 2131 /** 2132 * @return iterable<PropertyInfo> 2133 */ 2134 public function getAllPropertyInfos(): iterable { 2135 foreach ($this->classInfos as $classInfo) { 2136 yield from $classInfo->propertyInfos; 2137 } 2138 } 2139 2140 public function __clone() 2141 { 2142 foreach ($this->funcInfos as $key => $funcInfo) { 2143 $this->funcInfos[$key] = clone $funcInfo; 2144 } 2145 2146 foreach ($this->classInfos as $key => $classInfo) { 2147 $this->classInfos[$key] = clone $classInfo; 2148 } 2149 } 2150} 2151 2152class DocCommentTag { 2153 /** @var string */ 2154 public $name; 2155 /** @var string|null */ 2156 public $value; 2157 2158 public function __construct(string $name, ?string $value) { 2159 $this->name = $name; 2160 $this->value = $value; 2161 } 2162 2163 public function getValue(): string { 2164 if ($this->value === null) { 2165 throw new Exception("@$this->name does not have a value"); 2166 } 2167 2168 return $this->value; 2169 } 2170 2171 public function getType(): string { 2172 $value = $this->getValue(); 2173 2174 $matches = []; 2175 2176 if ($this->name === "param") { 2177 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)\s*\$\w+.*$/', $value, $matches); 2178 } elseif ($this->name === "return" || $this->name === "var") { 2179 preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)/', $value, $matches); 2180 } 2181 2182 if (!isset($matches[1])) { 2183 throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\""); 2184 } 2185 2186 return trim($matches[1]); 2187 } 2188 2189 public function getVariableName(): string { 2190 $value = $this->value; 2191 if ($value === null || strlen($value) === 0) { 2192 throw new Exception("@$this->name doesn't have any value"); 2193 } 2194 2195 $matches = []; 2196 2197 if ($this->name === "param") { 2198 preg_match('/^\s*[\w\|\\\\\[\]]+\s*\$(\w+).*$/', $value, $matches); 2199 } elseif ($this->name === "prefer-ref") { 2200 preg_match('/^\s*\$(\w+).*$/', $value, $matches); 2201 } 2202 2203 if (!isset($matches[1])) { 2204 throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\""); 2205 } 2206 2207 return $matches[1]; 2208 } 2209} 2210 2211/** @return DocCommentTag[] */ 2212function parseDocComment(DocComment $comment): array { 2213 $commentText = substr($comment->getText(), 2, -2); 2214 $tags = []; 2215 foreach (explode("\n", $commentText) as $commentLine) { 2216 $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/'; 2217 if (preg_match($regex, trim($commentLine), $matches)) { 2218 $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null); 2219 } 2220 } 2221 2222 return $tags; 2223} 2224 2225function parseFunctionLike( 2226 PrettyPrinterAbstract $prettyPrinter, 2227 FunctionOrMethodName $name, 2228 int $classFlags, 2229 int $flags, 2230 Node\FunctionLike $func, 2231 ?string $cond 2232): FuncInfo { 2233 try { 2234 $comment = $func->getDocComment(); 2235 $paramMeta = []; 2236 $aliasType = null; 2237 $alias = null; 2238 $isDeprecated = false; 2239 $verify = true; 2240 $docReturnType = null; 2241 $tentativeReturnType = false; 2242 $docParamTypes = []; 2243 $refcount = null; 2244 2245 if ($comment) { 2246 $tags = parseDocComment($comment); 2247 foreach ($tags as $tag) { 2248 if ($tag->name === 'prefer-ref') { 2249 $varName = $tag->getVariableName(); 2250 if (!isset($paramMeta[$varName])) { 2251 $paramMeta[$varName] = []; 2252 } 2253 $paramMeta[$varName]['preferRef'] = true; 2254 } else if ($tag->name === 'alias' || $tag->name === 'implementation-alias') { 2255 $aliasType = $tag->name; 2256 $aliasParts = explode("::", $tag->getValue()); 2257 if (count($aliasParts) === 1) { 2258 $alias = new FunctionName(new Name($aliasParts[0])); 2259 } else { 2260 $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]); 2261 } 2262 } else if ($tag->name === 'deprecated') { 2263 $isDeprecated = true; 2264 } else if ($tag->name === 'no-verify') { 2265 $verify = false; 2266 } else if ($tag->name === 'tentative-return-type') { 2267 $tentativeReturnType = true; 2268 } else if ($tag->name === 'return') { 2269 $docReturnType = $tag->getType(); 2270 } else if ($tag->name === 'param') { 2271 $docParamTypes[$tag->getVariableName()] = $tag->getType(); 2272 } else if ($tag->name === 'refcount') { 2273 $refcount = $tag->getValue(); 2274 } 2275 } 2276 } 2277 2278 $varNameSet = []; 2279 $args = []; 2280 $numRequiredArgs = 0; 2281 $foundVariadic = false; 2282 foreach ($func->getParams() as $i => $param) { 2283 $varName = $param->var->name; 2284 $preferRef = !empty($paramMeta[$varName]['preferRef']); 2285 unset($paramMeta[$varName]); 2286 2287 if (isset($varNameSet[$varName])) { 2288 throw new Exception("Duplicate parameter name $varName"); 2289 } 2290 $varNameSet[$varName] = true; 2291 2292 if ($preferRef) { 2293 $sendBy = ArgInfo::SEND_PREFER_REF; 2294 } else if ($param->byRef) { 2295 $sendBy = ArgInfo::SEND_BY_REF; 2296 } else { 2297 $sendBy = ArgInfo::SEND_BY_VAL; 2298 } 2299 2300 if ($foundVariadic) { 2301 throw new Exception("Only the last parameter can be variadic"); 2302 } 2303 2304 $type = $param->type ? Type::fromNode($param->type) : null; 2305 if ($type === null && !isset($docParamTypes[$varName])) { 2306 throw new Exception("Missing parameter type"); 2307 } 2308 2309 if ($param->default instanceof Expr\ConstFetch && 2310 $param->default->name->toLowerString() === "null" && 2311 $type && !$type->isNullable() 2312 ) { 2313 $simpleType = $type->tryToSimpleType(); 2314 if ($simpleType === null) { 2315 throw new Exception("Parameter $varName has null default, but is not nullable"); 2316 } 2317 } 2318 2319 if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { 2320 throw new Exception('The exact class name must be used instead of "self"'); 2321 } 2322 2323 $foundVariadic = $param->variadic; 2324 2325 $args[] = new ArgInfo( 2326 $varName, 2327 $sendBy, 2328 $param->variadic, 2329 $type, 2330 isset($docParamTypes[$varName]) ? Type::fromString($docParamTypes[$varName]) : null, 2331 $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null 2332 ); 2333 if (!$param->default && !$param->variadic) { 2334 $numRequiredArgs = $i + 1; 2335 } 2336 } 2337 2338 foreach (array_keys($paramMeta) as $var) { 2339 throw new Exception("Found metadata for invalid param $var"); 2340 } 2341 2342 $returnType = $func->getReturnType(); 2343 if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { 2344 throw new Exception("Missing return type"); 2345 } 2346 2347 $return = new ReturnInfo( 2348 $func->returnsByRef(), 2349 $returnType ? Type::fromNode($returnType) : null, 2350 $docReturnType ? Type::fromString($docReturnType) : null, 2351 $tentativeReturnType, 2352 $refcount 2353 ); 2354 2355 return new FuncInfo( 2356 $name, 2357 $classFlags, 2358 $flags, 2359 $aliasType, 2360 $alias, 2361 $isDeprecated, 2362 $verify, 2363 $args, 2364 $return, 2365 $numRequiredArgs, 2366 $cond 2367 ); 2368 } catch (Exception $e) { 2369 throw new Exception($name . "(): " .$e->getMessage()); 2370 } 2371} 2372 2373function parseProperty( 2374 Name $class, 2375 int $flags, 2376 Stmt\PropertyProperty $property, 2377 ?Node $type, 2378 ?DocComment $comment, 2379 PrettyPrinterAbstract $prettyPrinter 2380): PropertyInfo { 2381 $phpDocType = null; 2382 $isDocReadonly = false; 2383 $link = null; 2384 2385 if ($comment) { 2386 $tags = parseDocComment($comment); 2387 foreach ($tags as $tag) { 2388 if ($tag->name === 'var') { 2389 $phpDocType = $tag->getType(); 2390 } elseif ($tag->name === 'readonly') { 2391 $isDocReadonly = true; 2392 } elseif ($tag->name === 'link') { 2393 $link = $tag->value; 2394 } 2395 } 2396 } 2397 2398 $propertyType = $type ? Type::fromNode($type) : null; 2399 if ($propertyType === null && !$phpDocType) { 2400 throw new Exception("Missing type for property $class::\$$property->name"); 2401 } 2402 2403 if ($property->default instanceof Expr\ConstFetch && 2404 $property->default->name->toLowerString() === "null" && 2405 $propertyType && !$propertyType->isNullable() 2406 ) { 2407 $simpleType = $propertyType->tryToSimpleType(); 2408 if ($simpleType === null) { 2409 throw new Exception( 2410 "Property $class::\$$property->name has null default, but is not nullable"); 2411 } 2412 } 2413 2414 return new PropertyInfo( 2415 new PropertyName($class, $property->name->__toString()), 2416 $flags, 2417 $propertyType, 2418 $phpDocType ? Type::fromString($phpDocType) : null, 2419 $property->default, 2420 $property->default ? $prettyPrinter->prettyPrintExpr($property->default) : null, 2421 $isDocReadonly, 2422 $link 2423 ); 2424} 2425 2426/** 2427 * @param PropertyInfo[] $properties 2428 * @param FuncInfo[] $methods 2429 * @param EnumCaseInfo[] $enumCases 2430 */ 2431function parseClass( 2432 Name $name, Stmt\ClassLike $class, array $properties, array $methods, array $enumCases 2433): ClassInfo { 2434 $flags = $class instanceof Class_ ? $class->flags : 0; 2435 $comment = $class->getDocComment(); 2436 $alias = null; 2437 $isDeprecated = false; 2438 $isStrictProperties = false; 2439 $isNotSerializable = false; 2440 2441 if ($comment) { 2442 $tags = parseDocComment($comment); 2443 foreach ($tags as $tag) { 2444 if ($tag->name === 'alias') { 2445 $alias = $tag->getValue(); 2446 } else if ($tag->name === 'deprecated') { 2447 $isDeprecated = true; 2448 } else if ($tag->name === 'strict-properties') { 2449 $isStrictProperties = true; 2450 } else if ($tag->name === 'not-serializable') { 2451 $isNotSerializable = true; 2452 } 2453 } 2454 } 2455 2456 $extends = []; 2457 $implements = []; 2458 2459 if ($class instanceof Class_) { 2460 $classKind = "class"; 2461 if ($class->extends) { 2462 $extends[] = $class->extends; 2463 } 2464 $implements = $class->implements; 2465 } elseif ($class instanceof Interface_) { 2466 $classKind = "interface"; 2467 $extends = $class->extends; 2468 } else if ($class instanceof Trait_) { 2469 $classKind = "trait"; 2470 } else if ($class instanceof Enum_) { 2471 $classKind = "enum"; 2472 $implements = $class->implements; 2473 } else { 2474 throw new Exception("Unknown class kind " . get_class($class)); 2475 } 2476 2477 return new ClassInfo( 2478 $name, 2479 $flags, 2480 $classKind, 2481 $alias, 2482 $class instanceof Enum_ && $class->scalarType !== null 2483 ? SimpleType::fromNode($class->scalarType) : null, 2484 $isDeprecated, 2485 $isStrictProperties, 2486 $isNotSerializable, 2487 $extends, 2488 $implements, 2489 $properties, 2490 $methods, 2491 $enumCases 2492 ); 2493} 2494 2495function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string { 2496 foreach ($stmt->getComments() as $comment) { 2497 $text = trim($comment->getText()); 2498 if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) { 2499 $conds[] = $matches[1]; 2500 } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) { 2501 $conds[] = "defined($matches[1])"; 2502 } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) { 2503 $conds[] = "!defined($matches[1])"; 2504 } else if (preg_match('/^#\s*else$/', $text)) { 2505 if (empty($conds)) { 2506 throw new Exception("Encountered else without corresponding #if"); 2507 } 2508 $cond = array_pop($conds); 2509 $conds[] = "!($cond)"; 2510 } else if (preg_match('/^#\s*endif$/', $text)) { 2511 if (empty($conds)) { 2512 throw new Exception("Encountered #endif without corresponding #if"); 2513 } 2514 array_pop($conds); 2515 } else if ($text[0] === '#') { 2516 throw new Exception("Unrecognized preprocessor directive \"$text\""); 2517 } 2518 } 2519 2520 return empty($conds) ? null : implode(' && ', $conds); 2521} 2522 2523function getFileDocComment(array $stmts): ?DocComment { 2524 if (empty($stmts)) { 2525 return null; 2526 } 2527 2528 $comments = $stmts[0]->getComments(); 2529 if (empty($comments)) { 2530 return null; 2531 } 2532 2533 if ($comments[0] instanceof DocComment) { 2534 return $comments[0]; 2535 } 2536 2537 return null; 2538} 2539 2540function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) { 2541 $conds = []; 2542 foreach ($stmts as $stmt) { 2543 if ($stmt instanceof Stmt\Nop) { 2544 continue; 2545 } 2546 2547 if ($stmt instanceof Stmt\Namespace_) { 2548 handleStatements($fileInfo, $stmt->stmts, $prettyPrinter); 2549 continue; 2550 } 2551 2552 $cond = handlePreprocessorConditions($conds, $stmt); 2553 if ($stmt instanceof Stmt\Function_) { 2554 $fileInfo->funcInfos[] = parseFunctionLike( 2555 $prettyPrinter, 2556 new FunctionName($stmt->namespacedName), 2557 0, 2558 0, 2559 $stmt, 2560 $cond 2561 ); 2562 continue; 2563 } 2564 2565 if ($stmt instanceof Stmt\ClassLike) { 2566 $className = $stmt->namespacedName; 2567 $propertyInfos = []; 2568 $methodInfos = []; 2569 $enumCaseInfos = []; 2570 foreach ($stmt->stmts as $classStmt) { 2571 $cond = handlePreprocessorConditions($conds, $classStmt); 2572 if ($classStmt instanceof Stmt\Nop) { 2573 continue; 2574 } 2575 2576 $classFlags = $stmt instanceof Class_ ? $stmt->flags : 0; 2577 $abstractFlag = $stmt instanceof Stmt\Interface_ ? Class_::MODIFIER_ABSTRACT : 0; 2578 2579 if ($classStmt instanceof Stmt\Property) { 2580 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 2581 throw new Exception("Visibility modifier is required"); 2582 } 2583 foreach ($classStmt->props as $property) { 2584 $propertyInfos[] = parseProperty( 2585 $className, 2586 $classStmt->flags, 2587 $property, 2588 $classStmt->type, 2589 $classStmt->getDocComment(), 2590 $prettyPrinter 2591 ); 2592 } 2593 } else if ($classStmt instanceof Stmt\ClassMethod) { 2594 if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) { 2595 throw new Exception("Visibility modifier is required"); 2596 } 2597 $methodInfos[] = parseFunctionLike( 2598 $prettyPrinter, 2599 new MethodName($className, $classStmt->name->toString()), 2600 $classFlags, 2601 $classStmt->flags | $abstractFlag, 2602 $classStmt, 2603 $cond 2604 ); 2605 } else if ($classStmt instanceof Stmt\EnumCase) { 2606 $enumCaseInfos[] = new EnumCaseInfo( 2607 $classStmt->name->toString(), $classStmt->expr); 2608 } else { 2609 throw new Exception("Not implemented {$classStmt->getType()}"); 2610 } 2611 } 2612 2613 $fileInfo->classInfos[] = parseClass( 2614 $className, $stmt, $propertyInfos, $methodInfos, $enumCaseInfos); 2615 continue; 2616 } 2617 2618 throw new Exception("Unexpected node {$stmt->getType()}"); 2619 } 2620} 2621 2622function parseStubFile(string $code): FileInfo { 2623 $lexer = new PhpParser\Lexer\Emulative(); 2624 $parser = new PhpParser\Parser\Php7($lexer); 2625 $nodeTraverser = new PhpParser\NodeTraverser; 2626 $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); 2627 $prettyPrinter = new class extends Standard { 2628 protected function pName_FullyQualified(Name\FullyQualified $node) { 2629 return implode('\\', $node->parts); 2630 } 2631 }; 2632 2633 $stmts = $parser->parse($code); 2634 $nodeTraverser->traverse($stmts); 2635 2636 $fileInfo = new FileInfo; 2637 $fileDocComment = getFileDocComment($stmts); 2638 if ($fileDocComment) { 2639 $fileTags = parseDocComment($fileDocComment); 2640 foreach ($fileTags as $tag) { 2641 if ($tag->name === 'generate-function-entries') { 2642 $fileInfo->generateFunctionEntries = true; 2643 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 2644 } else if ($tag->name === 'generate-legacy-arginfo') { 2645 $fileInfo->generateLegacyArginfo = true; 2646 } else if ($tag->name === 'generate-class-entries') { 2647 $fileInfo->generateClassEntries = true; 2648 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 2649 } 2650 } 2651 } 2652 2653 // Generating class entries require generating function/method entries 2654 if ($fileInfo->generateClassEntries && !$fileInfo->generateFunctionEntries) { 2655 $fileInfo->generateFunctionEntries = true; 2656 } 2657 2658 handleStatements($fileInfo, $stmts, $prettyPrinter); 2659 return $fileInfo; 2660} 2661 2662function funcInfoToCode(FuncInfo $funcInfo): string { 2663 $code = ''; 2664 $returnType = $funcInfo->return->type; 2665 $isTentativeReturnType = $funcInfo->return->tentativeReturnType; 2666 2667 if ($returnType !== null) { 2668 if (null !== $simpleReturnType = $returnType->tryToSimpleType()) { 2669 if ($simpleReturnType->isBuiltin) { 2670 $code .= sprintf( 2671 "%s(%s, %d, %d, %s, %d)\n", 2672 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX", 2673 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 2674 $funcInfo->numRequiredArgs, 2675 $simpleReturnType->toTypeCode(), $returnType->isNullable() 2676 ); 2677 } else { 2678 $code .= sprintf( 2679 "%s(%s, %d, %d, %s, %d)\n", 2680 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX", 2681 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 2682 $funcInfo->numRequiredArgs, 2683 $simpleReturnType->toEscapedName(), $returnType->isNullable() 2684 ); 2685 } 2686 } else { 2687 $arginfoType = $returnType->toArginfoType(); 2688 if ($arginfoType->hasClassType()) { 2689 $code .= sprintf( 2690 "%s(%s, %d, %d, %s, %s)\n", 2691 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX", 2692 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 2693 $funcInfo->numRequiredArgs, 2694 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask() 2695 ); 2696 } else { 2697 $code .= sprintf( 2698 "%s(%s, %d, %d, %s)\n", 2699 $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX", 2700 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 2701 $funcInfo->numRequiredArgs, 2702 $arginfoType->toTypeMask() 2703 ); 2704 } 2705 } 2706 } else { 2707 $code .= sprintf( 2708 "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", 2709 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 2710 ); 2711 } 2712 2713 foreach ($funcInfo->args as $argInfo) { 2714 $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG"; 2715 $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; 2716 $argType = $argInfo->type; 2717 if ($argType !== null) { 2718 if (null !== $simpleArgType = $argType->tryToSimpleType()) { 2719 if ($simpleArgType->isBuiltin) { 2720 $code .= sprintf( 2721 "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n", 2722 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 2723 $simpleArgType->toTypeCode(), $argType->isNullable(), 2724 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 2725 ); 2726 } else { 2727 $code .= sprintf( 2728 "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n", 2729 $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 2730 $simpleArgType->toEscapedName(), $argType->isNullable(), 2731 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 2732 ); 2733 } 2734 } else { 2735 $arginfoType = $argType->toArginfoType(); 2736 if ($arginfoType->hasClassType()) { 2737 $code .= sprintf( 2738 "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s%s)\n", 2739 $argKind, $argInfo->getSendByString(), $argInfo->name, 2740 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(), 2741 !$argInfo->isVariadic ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 2742 ); 2743 } else { 2744 $code .= sprintf( 2745 "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n", 2746 $argKind, $argInfo->getSendByString(), $argInfo->name, 2747 $arginfoType->toTypeMask(), 2748 $argInfo->getDefaultValueAsArginfoString() 2749 ); 2750 } 2751 } 2752 } else { 2753 $code .= sprintf( 2754 "\tZEND_%s_INFO%s(%s, %s%s)\n", 2755 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 2756 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 2757 ); 2758 } 2759 } 2760 2761 $code .= "ZEND_END_ARG_INFO()"; 2762 return $code . "\n"; 2763} 2764 2765/** @param FuncInfo[] $generatedFuncInfos */ 2766function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo { 2767 foreach ($generatedFuncInfos as $generatedFuncInfo) { 2768 if ($generatedFuncInfo->equalsApartFromNameAndRefcount($funcInfo)) { 2769 return $generatedFuncInfo; 2770 } 2771 } 2772 return null; 2773} 2774 2775/** @param iterable<FuncInfo> $funcInfos */ 2776function generateCodeWithConditions( 2777 iterable $funcInfos, string $separator, Closure $codeGenerator): string { 2778 $code = ""; 2779 foreach ($funcInfos as $funcInfo) { 2780 $funcCode = $codeGenerator($funcInfo); 2781 if ($funcCode === null) { 2782 continue; 2783 } 2784 2785 $code .= $separator; 2786 if ($funcInfo->cond) { 2787 $code .= "#if {$funcInfo->cond}\n"; 2788 $code .= $funcCode; 2789 $code .= "#endif\n"; 2790 } else { 2791 $code .= $funcCode; 2792 } 2793 } 2794 return $code; 2795} 2796 2797function generateArgInfoCode(FileInfo $fileInfo, string $stubHash): string { 2798 $code = "/* This is a generated file, edit the .stub.php file instead.\n" 2799 . " * Stub hash: $stubHash */\n"; 2800 $generatedFuncInfos = []; 2801 $code .= generateCodeWithConditions( 2802 $fileInfo->getAllFuncInfos(), "\n", 2803 function (FuncInfo $funcInfo) use(&$generatedFuncInfos) { 2804 /* If there already is an equivalent arginfo structure, only emit a #define */ 2805 if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) { 2806 $code = sprintf( 2807 "#define %s %s\n", 2808 $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() 2809 ); 2810 } else { 2811 $code = funcInfoToCode($funcInfo); 2812 } 2813 2814 $generatedFuncInfos[] = $funcInfo; 2815 return $code; 2816 } 2817 ); 2818 2819 if ($fileInfo->generateFunctionEntries) { 2820 $code .= "\n\n"; 2821 2822 $generatedFunctionDeclarations = []; 2823 $code .= generateCodeWithConditions( 2824 $fileInfo->getAllFuncInfos(), "", 2825 function (FuncInfo $funcInfo) use($fileInfo, &$generatedFunctionDeclarations) { 2826 $key = $funcInfo->getDeclarationKey(); 2827 if (isset($generatedFunctionDeclarations[$key])) { 2828 return null; 2829 } 2830 2831 $generatedFunctionDeclarations[$key] = true; 2832 return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); 2833 } 2834 ); 2835 2836 if (!empty($fileInfo->funcInfos)) { 2837 $code .= generateFunctionEntries(null, $fileInfo->funcInfos); 2838 } 2839 2840 foreach ($fileInfo->classInfos as $classInfo) { 2841 $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos); 2842 } 2843 } 2844 2845 if ($fileInfo->generateClassEntries) { 2846 $code .= generateClassEntryCode($fileInfo); 2847 } 2848 2849 return $code; 2850} 2851 2852function generateClassEntryCode(FileInfo $fileInfo): string { 2853 $code = ""; 2854 2855 foreach ($fileInfo->classInfos as $class) { 2856 $code .= "\n" . $class->getRegistration(); 2857 } 2858 2859 return $code; 2860} 2861 2862/** @param FuncInfo[] $funcInfos */ 2863function generateFunctionEntries(?Name $className, array $funcInfos): string { 2864 $code = ""; 2865 2866 $functionEntryName = "ext_functions"; 2867 if ($className) { 2868 $underscoreName = implode("_", $className->parts); 2869 $functionEntryName = "class_{$underscoreName}_methods"; 2870 } 2871 2872 $code .= "\n\nstatic const zend_function_entry {$functionEntryName}[] = {\n"; 2873 $code .= generateCodeWithConditions($funcInfos, "", function (FuncInfo $funcInfo) { 2874 return $funcInfo->getFunctionEntry(); 2875 }); 2876 $code .= "\tZEND_FE_END\n"; 2877 $code .= "};\n"; 2878 2879 return $code; 2880} 2881 2882/** @param FuncInfo<string, FuncInfo> $funcInfos */ 2883function generateOptimizerInfo(array $funcInfos): string { 2884 2885 $code = "/* This is a generated file, edit the .stub.php files instead. */\n\n"; 2886 2887 $code .= "static const func_info_t func_infos[] = {\n"; 2888 2889 $code .= generateCodeWithConditions($funcInfos, "", function (FuncInfo $funcInfo) { 2890 return $funcInfo->getOptimizerInfo(); 2891 }); 2892 2893 $code .= "};\n"; 2894 2895 return $code; 2896} 2897 2898/** 2899 * @param ClassInfo[] $classMap 2900 * @return array<string, string> 2901 */ 2902function generateClassSynopses(array $classMap): array { 2903 $result = []; 2904 2905 foreach ($classMap as $classInfo) { 2906 $classSynopsis = $classInfo->getClassSynopsisDocument($classMap); 2907 if ($classSynopsis !== null) { 2908 $result[ClassInfo::getClassSynopsisFilename($classInfo->name) . ".xml"] = $classSynopsis; 2909 } 2910 } 2911 2912 return $result; 2913} 2914 2915/** 2916 * @param ClassInfo[] $classMap 2917 * @return array<string, string> 2918 */ 2919function replaceClassSynopses(string $targetDirectory, array $classMap): array 2920{ 2921 $classSynopses = []; 2922 2923 $it = new RecursiveIteratorIterator( 2924 new RecursiveDirectoryIterator($targetDirectory), 2925 RecursiveIteratorIterator::LEAVES_ONLY 2926 ); 2927 2928 foreach ($it as $file) { 2929 $pathName = $file->getPathName(); 2930 if (!preg_match('/\.xml$/i', $pathName)) { 2931 continue; 2932 } 2933 2934 $xml = file_get_contents($pathName); 2935 if ($xml === false) { 2936 continue; 2937 } 2938 2939 if (stripos($xml, "<classsynopsis") === false) { 2940 continue; 2941 } 2942 2943 $replacedXml = getReplacedSynopsisXml($xml); 2944 2945 $doc = new DOMDocument(); 2946 $doc->formatOutput = false; 2947 $doc->preserveWhiteSpace = true; 2948 $doc->validateOnParse = true; 2949 $success = $doc->loadXML($replacedXml); 2950 if (!$success) { 2951 echo "Failed opening $pathName\n"; 2952 continue; 2953 } 2954 2955 $classSynopsisElements = []; 2956 foreach ($doc->getElementsByTagName("classsynopsis") as $element) { 2957 $classSynopsisElements[] = $element; 2958 } 2959 2960 foreach ($classSynopsisElements as $classSynopsis) { 2961 if (!$classSynopsis instanceof DOMElement) { 2962 continue; 2963 } 2964 2965 $firstChild = $classSynopsis->firstElementChild; 2966 if ($firstChild === null) { 2967 continue; 2968 } 2969 $firstChild = $firstChild->firstElementChild; 2970 if ($firstChild === null) { 2971 continue; 2972 } 2973 $className = $firstChild->textContent; 2974 if (!isset($classMap[$className])) { 2975 continue; 2976 } 2977 $classInfo = $classMap[$className]; 2978 2979 $newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap); 2980 if ($newClassSynopsis === null) { 2981 continue; 2982 } 2983 2984 // Check if there is any change - short circuit if there is not any. 2985 2986 if (replaceAndCompareXmls($doc, $classSynopsis, $newClassSynopsis)) { 2987 continue; 2988 } 2989 2990 // Return the updated XML 2991 2992 $replacedXml = $doc->saveXML(); 2993 2994 $replacedXml = preg_replace( 2995 [ 2996 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 2997 "/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 2998 "/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xi=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 2999 "/<phpdoc:(classref|exceptionref)\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xmlns:xi=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 3000 "/<phpdoc:(classref|exceptionref)\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xmlns:xi=\"([a-z0-9.:\/]+)\"\s+xmlns:phpdoc=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 3001 ], 3002 [ 3003 "&$1", 3004 "<phpdoc:$1 xml:id=\"$4\" xmlns:phpdoc=\"$2\" xmlns=\"$3\">", 3005 "<phpdoc:$1 xml:id=\"$5\" xmlns:phpdoc=\"$2\" xmlns=\"$3\" xmlns:xi=\"$4\">", 3006 "<phpdoc:$1 xml:id=\"$6\" xmlns:phpdoc=\"$2\" xmlns=\"$3\" xmlns:xlink=\"$4\" xmlns:xi=\"$5\">", 3007 "<phpdoc:$1 xml:id=\"$6\" xmlns:phpdoc=\"$5\" xmlns=\"$2\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">", 3008 ], 3009 $replacedXml 3010 ); 3011 3012 $classSynopses[$pathName] = $replacedXml; 3013 } 3014 } 3015 3016 return $classSynopses; 3017} 3018 3019function getReplacedSynopsisXml(string $xml): string 3020{ 3021 return preg_replace( 3022 [ 3023 "/&([A-Za-z0-9._{}%-]+?;)/", 3024 "/<(\/)*xi:([A-Za-z]+?)/" 3025 ], 3026 [ 3027 "REPLACED-ENTITY-$1", 3028 "<$1XI$2", 3029 ], 3030 $xml 3031 ); 3032} 3033 3034/** 3035 * @param array<string, FuncInfo> $funcMap 3036 * @param array<string, FuncInfo> $aliasMap 3037 * @return array<string, string> 3038 */ 3039function generateMethodSynopses(array $funcMap, array $aliasMap): array { 3040 $result = []; 3041 3042 foreach ($funcMap as $funcInfo) { 3043 $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap); 3044 if ($methodSynopsis !== null) { 3045 $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis; 3046 } 3047 } 3048 3049 return $result; 3050} 3051 3052/** 3053 * @param array<string, FuncInfo> $funcMap 3054 * @param array<string, FuncInfo> $aliasMap 3055 * @return array<string, string> 3056 */ 3057function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $aliasMap): array { 3058 $methodSynopses = []; 3059 3060 $it = new RecursiveIteratorIterator( 3061 new RecursiveDirectoryIterator($targetDirectory), 3062 RecursiveIteratorIterator::LEAVES_ONLY 3063 ); 3064 3065 foreach ($it as $file) { 3066 $pathName = $file->getPathName(); 3067 if (!preg_match('/\.xml$/i', $pathName)) { 3068 continue; 3069 } 3070 3071 $xml = file_get_contents($pathName); 3072 if ($xml === false) { 3073 continue; 3074 } 3075 3076 if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) { 3077 continue; 3078 } 3079 3080 $replacedXml = getReplacedSynopsisXml($xml); 3081 3082 $doc = new DOMDocument(); 3083 $doc->formatOutput = false; 3084 $doc->preserveWhiteSpace = true; 3085 $doc->validateOnParse = true; 3086 $success = $doc->loadXML($replacedXml); 3087 if (!$success) { 3088 echo "Failed opening $pathName\n"; 3089 continue; 3090 } 3091 3092 $methodSynopsisElements = []; 3093 foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) { 3094 $methodSynopsisElements[] = $element; 3095 } 3096 foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) { 3097 $methodSynopsisElements[] = $element; 3098 } 3099 foreach ($doc->getElementsByTagName("methodsynopsis") as $element) { 3100 $methodSynopsisElements[] = $element; 3101 } 3102 3103 foreach ($methodSynopsisElements as $methodSynopsis) { 3104 if (!$methodSynopsis instanceof DOMElement) { 3105 continue; 3106 } 3107 3108 $list = $methodSynopsis->getElementsByTagName("methodname"); 3109 $item = $list->item(0); 3110 if (!$item instanceof DOMElement) { 3111 continue; 3112 } 3113 $funcName = $item->textContent; 3114 if (!isset($funcMap[$funcName])) { 3115 continue; 3116 } 3117 $funcInfo = $funcMap[$funcName]; 3118 3119 $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 3120 if ($newMethodSynopsis === null) { 3121 continue; 3122 } 3123 3124 // Retrieve current signature 3125 3126 $params = []; 3127 $list = $methodSynopsis->getElementsByTagName("methodparam"); 3128 foreach ($list as $i => $item) { 3129 if (!$item instanceof DOMElement) { 3130 continue; 3131 } 3132 3133 $paramList = $item->getElementsByTagName("parameter"); 3134 if ($paramList->count() !== 1) { 3135 continue; 3136 } 3137 3138 $paramName = $paramList->item(0)->textContent; 3139 $paramTypes = []; 3140 3141 $paramList = $item->getElementsByTagName("type"); 3142 foreach ($paramList as $type) { 3143 if (!$type instanceof DOMElement) { 3144 continue; 3145 } 3146 3147 $paramTypes[] = $type->textContent; 3148 } 3149 3150 $params[$paramName] = ["index" => $i, "type" => $paramTypes]; 3151 } 3152 3153 // Check if there is any change - short circuit if there is not any. 3154 3155 if (replaceAndCompareXmls($doc, $methodSynopsis, $newMethodSynopsis)) { 3156 continue; 3157 } 3158 3159 // Update parameter references 3160 3161 $paramList = $doc->getElementsByTagName("parameter"); 3162 /** @var DOMElement $paramElement */ 3163 foreach ($paramList as $paramElement) { 3164 if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") { 3165 continue; 3166 } 3167 3168 $name = $paramElement->textContent; 3169 if (!isset($params[$name])) { 3170 continue; 3171 } 3172 3173 $index = $params[$name]["index"]; 3174 if (!isset($funcInfo->args[$index])) { 3175 continue; 3176 } 3177 3178 $paramElement->textContent = $funcInfo->args[$index]->name; 3179 } 3180 3181 // Return the updated XML 3182 3183 $replacedXml = $doc->saveXML(); 3184 3185 $replacedXml = preg_replace( 3186 [ 3187 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 3188 "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 3189 "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 3190 ], 3191 [ 3192 "&$1", 3193 "<refentry xml:id=\"$2\" xmlns=\"$1\">", 3194 "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 3195 ], 3196 $replacedXml 3197 ); 3198 3199 $methodSynopses[$pathName] = $replacedXml; 3200 } 3201 } 3202 3203 return $methodSynopses; 3204} 3205 3206function replaceAndCompareXmls(DOMDocument $doc, DOMElement $originalSynopsis, DOMElement $newSynopsis): bool 3207{ 3208 $docComparator = new DOMDocument(); 3209 $docComparator->preserveWhiteSpace = false; 3210 $docComparator->formatOutput = true; 3211 3212 $xml1 = $doc->saveXML($originalSynopsis); 3213 $xml1 = getReplacedSynopsisXml($xml1); 3214 $docComparator->loadXML($xml1); 3215 $xml1 = $docComparator->saveXML(); 3216 3217 $originalSynopsis->parentNode->replaceChild($newSynopsis, $originalSynopsis); 3218 3219 $xml2 = $doc->saveXML($newSynopsis); 3220 $xml2 = getReplacedSynopsisXml($xml2); 3221 3222 $docComparator->loadXML($xml2); 3223 $xml2 = $docComparator->saveXML(); 3224 3225 return $xml1 === $xml2; 3226} 3227 3228function installPhpParser(string $version, string $phpParserDir) { 3229 $lockFile = __DIR__ . "/PHP-Parser-install-lock"; 3230 $lockFd = fopen($lockFile, 'w+'); 3231 if (!flock($lockFd, LOCK_EX)) { 3232 throw new Exception("Failed to acquire installation lock"); 3233 } 3234 3235 try { 3236 // Check whether a parallel process has already installed PHP-Parser. 3237 if (is_dir($phpParserDir)) { 3238 return; 3239 } 3240 3241 $cwd = getcwd(); 3242 chdir(__DIR__); 3243 3244 $tarName = "v$version.tar.gz"; 3245 passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 3246 if ($exit !== 0) { 3247 passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 3248 } 3249 if ($exit !== 0) { 3250 throw new Exception("Failed to download PHP-Parser tarball"); 3251 } 3252 if (!mkdir($phpParserDir)) { 3253 throw new Exception("Failed to create directory $phpParserDir"); 3254 } 3255 passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit); 3256 if ($exit !== 0) { 3257 throw new Exception("Failed to extract PHP-Parser tarball"); 3258 } 3259 unlink(__DIR__ . "/$tarName"); 3260 chdir($cwd); 3261 } finally { 3262 flock($lockFd, LOCK_UN); 3263 @unlink($lockFile); 3264 } 3265} 3266 3267function initPhpParser() { 3268 static $isInitialized = false; 3269 if ($isInitialized) { 3270 return; 3271 } 3272 3273 if (!extension_loaded("tokenizer")) { 3274 throw new Exception("The \"tokenizer\" extension is not available"); 3275 } 3276 3277 $isInitialized = true; 3278 $version = "4.13.0"; 3279 $phpParserDir = __DIR__ . "/PHP-Parser-$version"; 3280 if (!is_dir($phpParserDir)) { 3281 installPhpParser($version, $phpParserDir); 3282 } 3283 3284 spl_autoload_register(function(string $class) use($phpParserDir) { 3285 if (strpos($class, "PhpParser\\") === 0) { 3286 $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php"; 3287 require $fileName; 3288 } 3289 }); 3290} 3291 3292$optind = null; 3293$options = getopt( 3294 "fh", 3295 [ 3296 "force-regeneration", "parameter-stats", "help", "verify", "generate-classsynopses", "replace-classsynopses", 3297 "generate-methodsynopses", "replace-methodsynopses", "generate-optimizer-info" 3298 ], 3299 $optind 3300); 3301 3302$context = new Context; 3303$printParameterStats = isset($options["parameter-stats"]); 3304$verify = isset($options["verify"]); 3305$generateClassSynopses = isset($options["generate-classsynopses"]); 3306$replaceClassSynopses = isset($options["replace-classsynopses"]); 3307$generateMethodSynopses = isset($options["generate-methodsynopses"]); 3308$replaceMethodSynopses = isset($options["replace-methodsynopses"]); 3309$generateOptimizerInfo = isset($options["generate-optimizer-info"]); 3310$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); 3311$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateClassSynopses || $generateOptimizerInfo || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses; 3312 3313$targetSynopses = $argv[$argc - 1] ?? null; 3314if ($replaceClassSynopses && $targetSynopses === null) { 3315 die("A target class synopsis directory must be provided for.\n"); 3316} 3317 3318if ($replaceMethodSynopses && $targetSynopses === null) { 3319 die("A target method synopsis directory must be provided.\n"); 3320} 3321 3322if (isset($options["h"]) || isset($options["help"])) { 3323 die("\nusage: gen_stub.php [ -f | --force-regeneration ] [ --generate-classsynopses ] [ --replace-classsynopses ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ --generate-optimizer-info ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n"); 3324} 3325 3326$fileInfos = []; 3327$locations = array_slice($argv, $optind) ?: ['.']; 3328foreach (array_unique($locations) as $location) { 3329 if (is_file($location)) { 3330 // Generate single file. 3331 $fileInfo = processStubFile($location, $context); 3332 if ($fileInfo) { 3333 $fileInfos[] = $fileInfo; 3334 } 3335 } else if (is_dir($location)) { 3336 array_push($fileInfos, ...processDirectory($location, $context)); 3337 } else { 3338 echo "$location is neither a file nor a directory.\n"; 3339 exit(1); 3340 } 3341} 3342 3343if ($printParameterStats) { 3344 $parameterStats = []; 3345 3346 foreach ($fileInfos as $fileInfo) { 3347 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 3348 foreach ($funcInfo->args as $argInfo) { 3349 if (!isset($parameterStats[$argInfo->name])) { 3350 $parameterStats[$argInfo->name] = 0; 3351 } 3352 $parameterStats[$argInfo->name]++; 3353 } 3354 } 3355 } 3356 3357 arsort($parameterStats); 3358 echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; 3359} 3360 3361/** @var array<string, ClassInfo> $classMap */ 3362$classMap = []; 3363/** @var array<string, FuncInfo> $funcMap */ 3364$funcMap = []; 3365/** @var array<string, FuncInfo> $aliasMap */ 3366$aliasMap = []; 3367 3368foreach ($fileInfos as $fileInfo) { 3369 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 3370 /** @var FuncInfo $funcInfo */ 3371 $funcMap[$funcInfo->name->__toString()] = $funcInfo; 3372 3373 // TODO: Don't use aliasMap for methodsynopsis? 3374 if ($funcInfo->aliasType === "alias") { 3375 $aliasMap[$funcInfo->alias->__toString()] = $funcInfo; 3376 } 3377 } 3378 3379 foreach ($fileInfo->classInfos as $classInfo) { 3380 $classMap[$classInfo->name->__toString()] = $classInfo; 3381 } 3382} 3383 3384if ($verify) { 3385 $errors = []; 3386 3387 foreach ($funcMap as $aliasFunc) { 3388 if (!$aliasFunc->alias) { 3389 continue; 3390 } 3391 3392 if (!isset($funcMap[$aliasFunc->alias->__toString()])) { 3393 $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found"; 3394 continue; 3395 } 3396 3397 if (!$aliasFunc->verify) { 3398 continue; 3399 } 3400 3401 $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()]; 3402 $aliasedArgs = $aliasedFunc->args; 3403 $aliasArgs = $aliasFunc->args; 3404 3405 if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) { 3406 if ($aliasFunc->isInstanceMethod()) { 3407 $aliasedArgs = array_slice($aliasedArgs, 1); 3408 } 3409 3410 if ($aliasedFunc->isInstanceMethod()) { 3411 $aliasArgs = array_slice($aliasArgs, 1); 3412 } 3413 } 3414 3415 array_map( 3416 function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { 3417 if ($aliasArg === null) { 3418 assert($aliasedArg !== null); 3419 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; 3420 return null; 3421 } 3422 3423 if ($aliasedArg === null) { 3424 $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; 3425 return null; 3426 } 3427 3428 if ($aliasArg->name !== $aliasedArg->name) { 3429 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; 3430 return null; 3431 } 3432 3433 if ($aliasArg->type != $aliasedArg->type) { 3434 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; 3435 } 3436 3437 if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { 3438 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; 3439 } 3440 }, 3441 $aliasArgs, $aliasedArgs 3442 ); 3443 3444 $aliasedReturn = $aliasedFunc->return; 3445 $aliasReturn = $aliasFunc->return; 3446 3447 if (!$aliasedFunc->name->isConstructor() && !$aliasFunc->name->isConstructor()) { 3448 $aliasedReturnType = $aliasedReturn->type ?? $aliasedReturn->phpDocType; 3449 $aliasReturnType = $aliasReturn->type ?? $aliasReturn->phpDocType; 3450 if ($aliasReturnType != $aliasedReturnType) { 3451 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type"; 3452 } 3453 } 3454 3455 $aliasedPhpDocReturnType = $aliasedReturn->phpDocType; 3456 $aliasPhpDocReturnType = $aliasReturn->phpDocType; 3457 if ($aliasedPhpDocReturnType != $aliasPhpDocReturnType && $aliasedPhpDocReturnType != $aliasReturn->type && $aliasPhpDocReturnType != $aliasedReturn->type) { 3458 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same PHPDoc return type"; 3459 } 3460 } 3461 3462 echo implode("\n", $errors); 3463 if (!empty($errors)) { 3464 echo "\n"; 3465 exit(1); 3466 } 3467} 3468 3469if ($generateClassSynopses) { 3470 $classSynopsesDirectory = getcwd() . "/classsynopses"; 3471 3472 $classSynopses = generateClassSynopses($classMap); 3473 if (!empty($classSynopses)) { 3474 if (!file_exists($classSynopsesDirectory)) { 3475 mkdir($classSynopsesDirectory); 3476 } 3477 3478 foreach ($classSynopses as $filename => $content) { 3479 if (file_put_contents("$classSynopsesDirectory/$filename", $content)) { 3480 echo "Saved $filename\n"; 3481 } 3482 } 3483 } 3484} 3485 3486if ($replaceClassSynopses) { 3487 $classSynopses = replaceClassSynopses($targetSynopses, $classMap); 3488 3489 foreach ($classSynopses as $filename => $content) { 3490 if (file_put_contents($filename, $content)) { 3491 echo "Saved $filename\n"; 3492 } 3493 } 3494} 3495 3496if ($generateMethodSynopses) { 3497 $methodSynopsesDirectory = getcwd() . "/methodsynopses"; 3498 3499 $methodSynopses = generateMethodSynopses($funcMap, $aliasMap); 3500 if (!empty($methodSynopses)) { 3501 if (!file_exists($methodSynopsesDirectory)) { 3502 mkdir($methodSynopsesDirectory); 3503 } 3504 3505 foreach ($methodSynopses as $filename => $content) { 3506 if (file_put_contents("$methodSynopsesDirectory/$filename", $content)) { 3507 echo "Saved $filename\n"; 3508 } 3509 } 3510 } 3511} 3512 3513if ($replaceMethodSynopses) { 3514 $methodSynopses = replaceMethodSynopses($targetSynopses, $funcMap, $aliasMap); 3515 3516 foreach ($methodSynopses as $filename => $content) { 3517 if (file_put_contents($filename, $content)) { 3518 echo "Saved $filename\n"; 3519 } 3520 } 3521} 3522 3523if ($generateOptimizerInfo) { 3524 $filename = dirname(__FILE__, 2) . "/Zend/Optimizer/zend_func_infos.h"; 3525 $optimizerInfo = generateOptimizerInfo($funcMap); 3526 3527 if (file_put_contents($filename, $optimizerInfo)) { 3528 echo "Saved $filename\n"; 3529 } 3530} 3531