1#!/usr/bin/env php 2<?php declare(strict_types=1); 3 4use PhpParser\Comment\Doc as DocComment; 5use PhpParser\Node; 6use PhpParser\Node\Expr; 7use PhpParser\Node\Name; 8use PhpParser\Node\Stmt; 9use PhpParser\Node\Stmt\Class_; 10use PhpParser\PrettyPrinter\Standard; 11use PhpParser\PrettyPrinterAbstract; 12 13error_reporting(E_ALL); 14 15/** 16 * @return FileInfo[] 17 */ 18function processDirectory(string $dir, Context $context): array { 19 $fileInfos = []; 20 21 $it = new RecursiveIteratorIterator( 22 new RecursiveDirectoryIterator($dir), 23 RecursiveIteratorIterator::LEAVES_ONLY 24 ); 25 foreach ($it as $file) { 26 $pathName = $file->getPathName(); 27 if (preg_match('/\.stub\.php$/', $pathName)) { 28 $fileInfo = processStubFile($pathName, $context); 29 if ($fileInfo) { 30 $fileInfos[] = $fileInfo; 31 } 32 } 33 } 34 35 return $fileInfos; 36} 37 38function processStubFile(string $stubFile, Context $context): ?FileInfo { 39 try { 40 if (!file_exists($stubFile)) { 41 throw new Exception("File $stubFile does not exist"); 42 } 43 44 $arginfoFile = str_replace('.stub.php', '_arginfo.h', $stubFile); 45 $legacyFile = str_replace('.stub.php', '_legacy_arginfo.h', $stubFile); 46 47 $stubCode = file_get_contents($stubFile); 48 $stubHash = computeStubHash($stubCode); 49 $oldStubHash = extractStubHash($arginfoFile); 50 if ($stubHash === $oldStubHash && !$context->forceParse) { 51 /* Stub file did not change, do not regenerate. */ 52 return null; 53 } 54 55 initPhpParser(); 56 $fileInfo = parseStubFile($stubCode); 57 $arginfoCode = generateArgInfoCode($fileInfo, $stubHash); 58 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) { 59 echo "Saved $arginfoFile\n"; 60 } 61 62 if ($fileInfo->generateLegacyArginfo) { 63 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 64 $funcInfo->discardInfoForOldPhpVersions(); 65 } 66 $arginfoCode = generateArgInfoCode($fileInfo, $stubHash); 67 if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) { 68 echo "Saved $legacyFile\n"; 69 } 70 } 71 72 return $fileInfo; 73 } catch (Exception $e) { 74 echo "In $stubFile:\n{$e->getMessage()}\n"; 75 exit(1); 76 } 77} 78 79function computeStubHash(string $stubCode): string { 80 return sha1(str_replace("\r\n", "\n", $stubCode)); 81} 82 83function extractStubHash(string $arginfoFile): ?string { 84 if (!file_exists($arginfoFile)) { 85 return null; 86 } 87 88 $arginfoCode = file_get_contents($arginfoFile); 89 if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) { 90 return null; 91 } 92 93 return $matches[1]; 94} 95 96class Context { 97 /** @var bool */ 98 public $forceParse = false; 99 /** @var bool */ 100 public $forceRegeneration = false; 101} 102 103class SimpleType { 104 /** @var string */ 105 public $name; 106 /** @var bool */ 107 public $isBuiltin; 108 109 public function __construct(string $name, bool $isBuiltin) { 110 $this->name = $name; 111 $this->isBuiltin = $isBuiltin; 112 } 113 114 public static function fromNode(Node $node): SimpleType { 115 if ($node instanceof Node\Name) { 116 if ($node->toLowerString() === 'static') { 117 // PHP internally considers "static" a builtin type. 118 return new SimpleType($node->toString(), true); 119 } 120 121 if ($node->toLowerString() === 'self') { 122 throw new Exception('The exact class name must be used instead of "self"'); 123 } 124 125 assert($node->isFullyQualified()); 126 return new SimpleType($node->toString(), false); 127 } 128 if ($node instanceof Node\Identifier) { 129 return new SimpleType($node->toString(), true); 130 } 131 throw new Exception("Unexpected node type"); 132 } 133 134 public static function fromPhpDoc(string $type): SimpleType 135 { 136 switch (strtolower($type)) { 137 case "void": 138 case "null": 139 case "false": 140 case "bool": 141 case "int": 142 case "float": 143 case "string": 144 case "array": 145 case "iterable": 146 case "object": 147 case "resource": 148 case "mixed": 149 case "static": 150 return new SimpleType(strtolower($type), true); 151 case "self": 152 throw new Exception('The exact class name must be used instead of "self"'); 153 } 154 155 if (strpos($type, "[]") !== false) { 156 return new SimpleType("array", true); 157 } 158 159 return new SimpleType($type, false); 160 } 161 162 public static function null(): SimpleType 163 { 164 return new SimpleType("null", true); 165 } 166 167 public static function void(): SimpleType 168 { 169 return new SimpleType("void", true); 170 } 171 172 public function isNull(): bool { 173 return $this->isBuiltin && $this->name === 'null'; 174 } 175 176 public function toTypeCode(): string { 177 assert($this->isBuiltin); 178 switch (strtolower($this->name)) { 179 case "bool": 180 return "_IS_BOOL"; 181 case "int": 182 return "IS_LONG"; 183 case "float": 184 return "IS_DOUBLE"; 185 case "string": 186 return "IS_STRING"; 187 case "array": 188 return "IS_ARRAY"; 189 case "object": 190 return "IS_OBJECT"; 191 case "void": 192 return "IS_VOID"; 193 case "callable": 194 return "IS_CALLABLE"; 195 case "iterable": 196 return "IS_ITERABLE"; 197 case "mixed": 198 return "IS_MIXED"; 199 case "static": 200 return "IS_STATIC"; 201 default: 202 throw new Exception("Not implemented: $this->name"); 203 } 204 } 205 206 public function toTypeMask() { 207 assert($this->isBuiltin); 208 switch (strtolower($this->name)) { 209 case "null": 210 return "MAY_BE_NULL"; 211 case "false": 212 return "MAY_BE_FALSE"; 213 case "bool": 214 return "MAY_BE_BOOL"; 215 case "int": 216 return "MAY_BE_LONG"; 217 case "float": 218 return "MAY_BE_DOUBLE"; 219 case "string": 220 return "MAY_BE_STRING"; 221 case "array": 222 return "MAY_BE_ARRAY"; 223 case "object": 224 return "MAY_BE_OBJECT"; 225 case "callable": 226 return "MAY_BE_CALLABLE"; 227 case "mixed": 228 return "MAY_BE_ANY"; 229 case "static": 230 return "MAY_BE_STATIC"; 231 default: 232 throw new Exception("Not implemented: $this->name"); 233 } 234 } 235 236 public function toEscapedName(): string { 237 return str_replace('\\', '\\\\', $this->name); 238 } 239 240 public function equals(SimpleType $other) { 241 return $this->name === $other->name 242 && $this->isBuiltin === $other->isBuiltin; 243 } 244} 245 246class Type { 247 /** @var SimpleType[] $types */ 248 public $types; 249 250 public function __construct(array $types) { 251 $this->types = $types; 252 } 253 254 public static function fromNode(Node $node): Type { 255 if ($node instanceof Node\UnionType) { 256 return new Type(array_map(['SimpleType', 'fromNode'], $node->types)); 257 } 258 if ($node instanceof Node\NullableType) { 259 return new Type([ 260 SimpleType::fromNode($node->type), 261 SimpleType::null(), 262 ]); 263 } 264 return new Type([SimpleType::fromNode($node)]); 265 } 266 267 public static function fromPhpDoc(string $phpDocType) { 268 $types = explode("|", $phpDocType); 269 270 $simpleTypes = []; 271 foreach ($types as $type) { 272 $simpleTypes[] = SimpleType::fromPhpDoc($type); 273 } 274 275 return new Type($simpleTypes); 276 } 277 278 public function isNullable(): bool { 279 foreach ($this->types as $type) { 280 if ($type->isNull()) { 281 return true; 282 } 283 } 284 return false; 285 } 286 287 public function getWithoutNull(): Type { 288 return new Type(array_filter($this->types, function(SimpleType $type) { 289 return !$type->isNull(); 290 })); 291 } 292 293 public function tryToSimpleType(): ?SimpleType { 294 $withoutNull = $this->getWithoutNull(); 295 if (count($withoutNull->types) === 1) { 296 return $withoutNull->types[0]; 297 } 298 return null; 299 } 300 301 public function toArginfoType(): ?ArginfoType { 302 $classTypes = []; 303 $builtinTypes = []; 304 foreach ($this->types as $type) { 305 if ($type->isBuiltin) { 306 $builtinTypes[] = $type; 307 } else { 308 $classTypes[] = $type; 309 } 310 } 311 return new ArginfoType($classTypes, $builtinTypes); 312 } 313 314 public static function equals(?Type $a, ?Type $b): bool { 315 if ($a === null || $b === null) { 316 return $a === $b; 317 } 318 319 if (count($a->types) !== count($b->types)) { 320 return false; 321 } 322 323 for ($i = 0; $i < count($a->types); $i++) { 324 if (!$a->types[$i]->equals($b->types[$i])) { 325 return false; 326 } 327 } 328 329 return true; 330 } 331 332 public function __toString() { 333 if ($this->types === null) { 334 return 'mixed'; 335 } 336 337 return implode('|', array_map( 338 function ($type) { return $type->name; }, 339 $this->types) 340 ); 341 } 342} 343 344class ArginfoType { 345 /** @var ClassType[] $classTypes */ 346 public $classTypes; 347 348 /** @var SimpleType[] $builtinTypes */ 349 private $builtinTypes; 350 351 public function __construct(array $classTypes, array $builtinTypes) { 352 $this->classTypes = $classTypes; 353 $this->builtinTypes = $builtinTypes; 354 } 355 356 public function hasClassType(): bool { 357 return !empty($this->classTypes); 358 } 359 360 public function toClassTypeString(): string { 361 return implode('|', array_map(function(SimpleType $type) { 362 return $type->toEscapedName(); 363 }, $this->classTypes)); 364 } 365 366 public function toTypeMask(): string { 367 if (empty($this->builtinTypes)) { 368 return '0'; 369 } 370 return implode('|', array_map(function(SimpleType $type) { 371 return $type->toTypeMask(); 372 }, $this->builtinTypes)); 373 } 374} 375 376class ArgInfo { 377 const SEND_BY_VAL = 0; 378 const SEND_BY_REF = 1; 379 const SEND_PREFER_REF = 2; 380 381 /** @var string */ 382 public $name; 383 /** @var int */ 384 public $sendBy; 385 /** @var bool */ 386 public $isVariadic; 387 /** @var Type|null */ 388 public $type; 389 /** @var Type|null */ 390 public $phpDocType; 391 /** @var string|null */ 392 public $defaultValue; 393 394 public function __construct(string $name, int $sendBy, bool $isVariadic, ?Type $type, ?Type $phpDocType, ?string $defaultValue) { 395 $this->name = $name; 396 $this->sendBy = $sendBy; 397 $this->isVariadic = $isVariadic; 398 $this->type = $type; 399 $this->phpDocType = $phpDocType; 400 $this->defaultValue = $defaultValue; 401 } 402 403 public function equals(ArgInfo $other): bool { 404 return $this->name === $other->name 405 && $this->sendBy === $other->sendBy 406 && $this->isVariadic === $other->isVariadic 407 && Type::equals($this->type, $other->type) 408 && $this->defaultValue === $other->defaultValue; 409 } 410 411 public function getSendByString(): string { 412 switch ($this->sendBy) { 413 case self::SEND_BY_VAL: 414 return "0"; 415 case self::SEND_BY_REF: 416 return "1"; 417 case self::SEND_PREFER_REF: 418 return "ZEND_SEND_PREFER_REF"; 419 } 420 throw new Exception("Invalid sendBy value"); 421 } 422 423 public function getMethodSynopsisType(): Type { 424 if ($this->type) { 425 return $this->type; 426 } 427 428 if ($this->phpDocType) { 429 return $this->phpDocType; 430 } 431 432 throw new Exception("A parameter must have a type"); 433 } 434 435 public function hasProperDefaultValue(): bool { 436 return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN"; 437 } 438 439 public function getDefaultValueAsArginfoString(): string { 440 if ($this->hasProperDefaultValue()) { 441 return '"' . addslashes($this->defaultValue) . '"'; 442 } 443 444 return "NULL"; 445 } 446 447 public function getDefaultValueAsMethodSynopsisString(): ?string { 448 if ($this->defaultValue === null) { 449 return null; 450 } 451 452 switch ($this->defaultValue) { 453 case 'UNKNOWN': 454 return null; 455 case 'false': 456 case 'true': 457 case 'null': 458 return "&{$this->defaultValue};"; 459 } 460 461 return $this->defaultValue; 462 } 463} 464 465interface FunctionOrMethodName { 466 public function getDeclaration(): string; 467 public function getArgInfoName(): string; 468 public function getMethodSynopsisFilename(): string; 469 public function __toString(): string; 470 public function isMethod(): bool; 471 public function isConstructor(): bool; 472 public function isDestructor(): bool; 473} 474 475class FunctionName implements FunctionOrMethodName { 476 /** @var Name */ 477 private $name; 478 479 public function __construct(Name $name) { 480 $this->name = $name; 481 } 482 483 public function getNamespace(): ?string { 484 if ($this->name->isQualified()) { 485 return $this->name->slice(0, -1)->toString(); 486 } 487 return null; 488 } 489 490 public function getNonNamespacedName(): string { 491 if ($this->name->isQualified()) { 492 throw new Exception("Namespaced name not supported here"); 493 } 494 return $this->name->toString(); 495 } 496 497 public function getDeclarationName(): string { 498 return $this->name->getLast(); 499 } 500 501 public function getDeclaration(): string { 502 return "ZEND_FUNCTION({$this->getDeclarationName()});\n"; 503 } 504 505 public function getArgInfoName(): string { 506 $underscoreName = implode('_', $this->name->parts); 507 return "arginfo_$underscoreName"; 508 } 509 510 public function getMethodSynopsisFilename(): string { 511 return implode('_', $this->name->parts); 512 } 513 514 public function __toString(): string { 515 return $this->name->toString(); 516 } 517 518 public function isMethod(): bool { 519 return false; 520 } 521 522 public function isConstructor(): bool { 523 return false; 524 } 525 526 public function isDestructor(): bool { 527 return false; 528 } 529} 530 531class MethodName implements FunctionOrMethodName { 532 /** @var Name */ 533 private $className; 534 /** @var string */ 535 public $methodName; 536 537 public function __construct(Name $className, string $methodName) { 538 $this->className = $className; 539 $this->methodName = $methodName; 540 } 541 542 public function getDeclarationClassName(): string { 543 return implode('_', $this->className->parts); 544 } 545 546 public function getDeclaration(): string { 547 return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n"; 548 } 549 550 public function getArgInfoName(): string { 551 return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}"; 552 } 553 554 public function getMethodSynopsisFilename(): string { 555 return $this->getDeclarationClassName() . "_{$this->methodName}"; 556 } 557 558 public function __toString(): string { 559 return "$this->className::$this->methodName"; 560 } 561 562 public function isMethod(): bool { 563 return true; 564 } 565 566 public function isConstructor(): bool { 567 return $this->methodName === "__construct"; 568 } 569 570 public function isDestructor(): bool { 571 return $this->methodName === "__destruct"; 572 } 573} 574 575class ReturnInfo { 576 /** @var bool */ 577 public $byRef; 578 /** @var Type|null */ 579 public $type; 580 /** @var Type|null */ 581 public $phpDocType; 582 583 public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType) { 584 $this->byRef = $byRef; 585 $this->type = $type; 586 $this->phpDocType = $phpDocType; 587 } 588 589 public function equals(ReturnInfo $other): bool { 590 return $this->byRef === $other->byRef 591 && Type::equals($this->type, $other->type); 592 } 593 594 public function getMethodSynopsisType(): ?Type { 595 return $this->type ?? $this->phpDocType; 596 } 597} 598 599class FuncInfo { 600 /** @var FunctionOrMethodName */ 601 public $name; 602 /** @var int */ 603 public $classFlags; 604 /** @var int */ 605 public $flags; 606 /** @var string|null */ 607 public $aliasType; 608 /** @var FunctionName|null */ 609 public $alias; 610 /** @var bool */ 611 public $isDeprecated; 612 /** @var bool */ 613 public $verify; 614 /** @var ArgInfo[] */ 615 public $args; 616 /** @var ReturnInfo */ 617 public $return; 618 /** @var int */ 619 public $numRequiredArgs; 620 /** @var string|null */ 621 public $cond; 622 623 public function __construct( 624 FunctionOrMethodName $name, 625 int $classFlags, 626 int $flags, 627 ?string $aliasType, 628 ?FunctionOrMethodName $alias, 629 bool $isDeprecated, 630 bool $verify, 631 array $args, 632 ReturnInfo $return, 633 int $numRequiredArgs, 634 ?string $cond 635 ) { 636 $this->name = $name; 637 $this->classFlags = $classFlags; 638 $this->flags = $flags; 639 $this->aliasType = $aliasType; 640 $this->alias = $alias; 641 $this->isDeprecated = $isDeprecated; 642 $this->verify = $verify; 643 $this->args = $args; 644 $this->return = $return; 645 $this->numRequiredArgs = $numRequiredArgs; 646 $this->cond = $cond; 647 } 648 649 public function isMethod(): bool 650 { 651 return $this->name->isMethod(); 652 } 653 654 public function isFinalMethod(): bool 655 { 656 return ($this->flags & Class_::MODIFIER_FINAL) || ($this->classFlags & Class_::MODIFIER_FINAL); 657 } 658 659 public function isInstanceMethod(): bool 660 { 661 return !($this->flags & Class_::MODIFIER_STATIC) && $this->isMethod() && !$this->name->isConstructor(); 662 } 663 664 /** @return string[] */ 665 public function getModifierNames(): array 666 { 667 if (!$this->isMethod()) { 668 return []; 669 } 670 671 $result = []; 672 673 if ($this->flags & Class_::MODIFIER_FINAL) { 674 $result[] = "final"; 675 } elseif ($this->flags & Class_::MODIFIER_ABSTRACT && $this->classFlags & ~Class_::MODIFIER_ABSTRACT) { 676 $result[] = "abstract"; 677 } 678 679 if ($this->flags & Class_::MODIFIER_PROTECTED) { 680 $result[] = "protected"; 681 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 682 $result[] = "private"; 683 } else { 684 $result[] = "public"; 685 } 686 687 if ($this->flags & Class_::MODIFIER_STATIC) { 688 $result[] = "static"; 689 } 690 691 return $result; 692 } 693 694 public function hasParamWithUnknownDefaultValue(): bool 695 { 696 foreach ($this->args as $arg) { 697 if ($arg->defaultValue && !$arg->hasProperDefaultValue()) { 698 return true; 699 } 700 } 701 702 return false; 703 } 704 705 public function equalsApartFromName(FuncInfo $other): bool { 706 if (count($this->args) !== count($other->args)) { 707 return false; 708 } 709 710 for ($i = 0; $i < count($this->args); $i++) { 711 if (!$this->args[$i]->equals($other->args[$i])) { 712 return false; 713 } 714 } 715 716 return $this->return->equals($other->return) 717 && $this->numRequiredArgs === $other->numRequiredArgs 718 && $this->cond === $other->cond; 719 } 720 721 public function getArgInfoName(): string { 722 return $this->name->getArgInfoName(); 723 } 724 725 public function getDeclarationKey(): string 726 { 727 $name = $this->alias ?? $this->name; 728 729 return "$name|$this->cond"; 730 } 731 732 public function getDeclaration(): ?string 733 { 734 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 735 return null; 736 } 737 738 $name = $this->alias ?? $this->name; 739 740 return $name->getDeclaration(); 741 } 742 743 public function getFunctionEntry(): string { 744 if ($this->name instanceof MethodName) { 745 if ($this->alias) { 746 if ($this->alias instanceof MethodName) { 747 return sprintf( 748 "\tZEND_MALIAS(%s, %s, %s, %s, %s)\n", 749 $this->alias->getDeclarationClassName(), $this->name->methodName, 750 $this->alias->methodName, $this->getArgInfoName(), $this->getFlagsAsArginfoString() 751 ); 752 } else if ($this->alias instanceof FunctionName) { 753 return sprintf( 754 "\tZEND_ME_MAPPING(%s, %s, %s, %s)\n", 755 $this->name->methodName, $this->alias->getNonNamespacedName(), 756 $this->getArgInfoName(), $this->getFlagsAsArginfoString() 757 ); 758 } else { 759 throw new Error("Cannot happen"); 760 } 761 } else { 762 $declarationClassName = $this->name->getDeclarationClassName(); 763 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 764 return sprintf( 765 "\tZEND_ABSTRACT_ME_WITH_FLAGS(%s, %s, %s, %s)\n", 766 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 767 $this->getFlagsAsArginfoString() 768 ); 769 } 770 771 return sprintf( 772 "\tZEND_ME(%s, %s, %s, %s)\n", 773 $declarationClassName, $this->name->methodName, $this->getArgInfoName(), 774 $this->getFlagsAsArginfoString() 775 ); 776 } 777 } else if ($this->name instanceof FunctionName) { 778 $namespace = $this->name->getNamespace(); 779 $declarationName = $this->name->getDeclarationName(); 780 781 if ($this->alias && $this->isDeprecated) { 782 return sprintf( 783 "\tZEND_DEP_FALIAS(%s, %s, %s)\n", 784 $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() 785 ); 786 } 787 788 if ($this->alias) { 789 return sprintf( 790 "\tZEND_FALIAS(%s, %s, %s)\n", 791 $declarationName, $this->alias->getNonNamespacedName(), $this->getArgInfoName() 792 ); 793 } 794 795 if ($this->isDeprecated) { 796 return sprintf( 797 "\tZEND_DEP_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); 798 } 799 800 if ($namespace) { 801 // Render A\B as "A\\B" in C strings for namespaces 802 return sprintf( 803 "\tZEND_NS_FE(\"%s\", %s, %s)\n", 804 addslashes($namespace), $declarationName, $this->getArgInfoName()); 805 } else { 806 return sprintf("\tZEND_FE(%s, %s)\n", $declarationName, $this->getArgInfoName()); 807 } 808 } else { 809 throw new Error("Cannot happen"); 810 } 811 } 812 813 private function getFlagsAsArginfoString(): string 814 { 815 $flags = "ZEND_ACC_PUBLIC"; 816 if ($this->flags & Class_::MODIFIER_PROTECTED) { 817 $flags = "ZEND_ACC_PROTECTED"; 818 } elseif ($this->flags & Class_::MODIFIER_PRIVATE) { 819 $flags = "ZEND_ACC_PRIVATE"; 820 } 821 822 if ($this->flags & Class_::MODIFIER_STATIC) { 823 $flags .= "|ZEND_ACC_STATIC"; 824 } 825 826 if ($this->flags & Class_::MODIFIER_FINAL) { 827 $flags .= "|ZEND_ACC_FINAL"; 828 } 829 830 if ($this->flags & Class_::MODIFIER_ABSTRACT) { 831 $flags .= "|ZEND_ACC_ABSTRACT"; 832 } 833 834 if ($this->isDeprecated) { 835 $flags .= "|ZEND_ACC_DEPRECATED"; 836 } 837 838 return $flags; 839 } 840 841 /** 842 * @param FuncInfo[] $funcMap 843 * @param FuncInfo[] $aliasMap 844 * @throws Exception 845 */ 846 public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string { 847 848 $doc = new DOMDocument(); 849 $doc->formatOutput = true; 850 $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 851 if (!$methodSynopsis) { 852 return null; 853 } 854 855 $doc->appendChild($methodSynopsis); 856 857 return $doc->saveXML(); 858 } 859 860 /** 861 * @param FuncInfo[] $funcMap 862 * @param FuncInfo[] $aliasMap 863 * @throws Exception 864 */ 865 public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement { 866 if ($this->hasParamWithUnknownDefaultValue()) { 867 return null; 868 } 869 870 if ($this->name->isConstructor()) { 871 $synopsisType = "constructorsynopsis"; 872 } elseif ($this->name->isDestructor()) { 873 $synopsisType = "destructorsynopsis"; 874 } else { 875 $synopsisType = "methodsynopsis"; 876 } 877 878 $methodSynopsis = $doc->createElement($synopsisType); 879 880 $aliasedFunc = $this->aliasType === "alias" && isset($funcMap[$this->alias->__toString()]) ? $funcMap[$this->alias->__toString()] : null; 881 $aliasFunc = $aliasMap[$this->name->__toString()] ?? null; 882 883 if (($this->aliasType === "alias" && $aliasedFunc !== null && $aliasedFunc->isMethod() !== $this->isMethod()) || 884 ($aliasFunc !== null && $aliasFunc->isMethod() !== $this->isMethod()) 885 ) { 886 $role = $doc->createAttribute("role"); 887 $role->value = $this->isMethod() ? "oop" : "procedural"; 888 $methodSynopsis->appendChild($role); 889 } 890 891 $methodSynopsis->appendChild(new DOMText("\n ")); 892 893 foreach ($this->getModifierNames() as $modifierString) { 894 $modifierElement = $doc->createElement('modifier', $modifierString); 895 $methodSynopsis->appendChild($modifierElement); 896 $methodSynopsis->appendChild(new DOMText(" ")); 897 } 898 899 $returnType = $this->return->getMethodSynopsisType(); 900 if ($returnType) { 901 $this->appendMethodSynopsisTypeToElement($doc, $methodSynopsis, $returnType); 902 } 903 904 $methodname = $doc->createElement('methodname', $this->name->__toString()); 905 $methodSynopsis->appendChild($methodname); 906 907 if (empty($this->args)) { 908 $methodSynopsis->appendChild(new DOMText("\n ")); 909 $void = $doc->createElement('void'); 910 $methodSynopsis->appendChild($void); 911 } else { 912 foreach ($this->args as $arg) { 913 $methodSynopsis->appendChild(new DOMText("\n ")); 914 $methodparam = $doc->createElement('methodparam'); 915 if ($arg->defaultValue !== null) { 916 $methodparam->setAttribute("choice", "opt"); 917 } 918 if ($arg->isVariadic) { 919 $methodparam->setAttribute("rep", "repeat"); 920 } 921 922 $methodSynopsis->appendChild($methodparam); 923 $this->appendMethodSynopsisTypeToElement($doc, $methodparam, $arg->getMethodSynopsisType()); 924 925 $parameter = $doc->createElement('parameter', $arg->name); 926 if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) { 927 $parameter->setAttribute("role", "reference"); 928 } 929 930 $methodparam->appendChild($parameter); 931 $defaultValue = $arg->getDefaultValueAsMethodSynopsisString(); 932 if ($defaultValue !== null) { 933 $initializer = $doc->createElement('initializer'); 934 if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) { 935 $constant = $doc->createElement('constant', $defaultValue); 936 $initializer->appendChild($constant); 937 } else { 938 $initializer->nodeValue = $defaultValue; 939 } 940 $methodparam->appendChild($initializer); 941 } 942 } 943 } 944 $methodSynopsis->appendChild(new DOMText("\n ")); 945 946 return $methodSynopsis; 947 } 948 949 public function discardInfoForOldPhpVersions(): void { 950 $this->return->type = null; 951 foreach ($this->args as $arg) { 952 $arg->type = null; 953 $arg->defaultValue = null; 954 } 955 } 956 957 private function appendMethodSynopsisTypeToElement(DOMDocument $doc, DOMElement $elementToAppend, Type $type) { 958 if (count($type->types) > 1) { 959 $typeElement = $doc->createElement('type'); 960 $typeElement->setAttribute("class", "union"); 961 962 foreach ($type->types as $type) { 963 $unionTypeElement = $doc->createElement('type', $type->name); 964 $typeElement->appendChild($unionTypeElement); 965 } 966 } else { 967 $typeElement = $doc->createElement('type', $type->types[0]->name); 968 } 969 970 $elementToAppend->appendChild($typeElement); 971 } 972} 973 974class ClassInfo { 975 /** @var Name */ 976 public $name; 977 /** @var FuncInfo[] */ 978 public $funcInfos; 979 980 public function __construct(Name $name, array $funcInfos) { 981 $this->name = $name; 982 $this->funcInfos = $funcInfos; 983 } 984} 985 986class FileInfo { 987 /** @var FuncInfo[] */ 988 public $funcInfos = []; 989 /** @var ClassInfo[] */ 990 public $classInfos = []; 991 /** @var bool */ 992 public $generateFunctionEntries = false; 993 /** @var string */ 994 public $declarationPrefix = ""; 995 /** @var bool */ 996 public $generateLegacyArginfo = false; 997 998 /** 999 * @return iterable<FuncInfo> 1000 */ 1001 public function getAllFuncInfos(): iterable { 1002 yield from $this->funcInfos; 1003 foreach ($this->classInfos as $classInfo) { 1004 yield from $classInfo->funcInfos; 1005 } 1006 } 1007} 1008 1009class DocCommentTag { 1010 /** @var string */ 1011 public $name; 1012 /** @var string|null */ 1013 public $value; 1014 1015 public function __construct(string $name, ?string $value) { 1016 $this->name = $name; 1017 $this->value = $value; 1018 } 1019 1020 public function getValue(): string { 1021 if ($this->value === null) { 1022 throw new Exception("@$this->name does not have a value"); 1023 } 1024 1025 return $this->value; 1026 } 1027 1028 public function getType(): string { 1029 $value = $this->getValue(); 1030 1031 $matches = []; 1032 1033 if ($this->name === "param") { 1034 preg_match('/^\s*([\w\|\\\\\[\]]+)\s*\$\w+.*$/', $value, $matches); 1035 } elseif ($this->name === "return") { 1036 preg_match('/^\s*([\w\|\\\\\[\]]+)\s*$/', $value, $matches); 1037 } 1038 1039 if (isset($matches[1]) === false) { 1040 throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\""); 1041 } 1042 1043 return $matches[1]; 1044 } 1045 1046 public function getVariableName(): string { 1047 $value = $this->value; 1048 if ($value === null || strlen($value) === 0) { 1049 throw new Exception("@$this->name doesn't have any value"); 1050 } 1051 1052 $matches = []; 1053 1054 if ($this->name === "param") { 1055 preg_match('/^\s*[\w\|\\\\\[\]]+\s*\$(\w+).*$/', $value, $matches); 1056 } elseif ($this->name === "prefer-ref") { 1057 preg_match('/^\s*\$(\w+).*$/', $value, $matches); 1058 } 1059 1060 if (isset($matches[1]) === false) { 1061 throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\""); 1062 } 1063 1064 return $matches[1]; 1065 } 1066} 1067 1068/** @return DocCommentTag[] */ 1069function parseDocComment(DocComment $comment): array { 1070 $commentText = substr($comment->getText(), 2, -2); 1071 $tags = []; 1072 foreach (explode("\n", $commentText) as $commentLine) { 1073 $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/'; 1074 if (preg_match($regex, trim($commentLine), $matches)) { 1075 $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null); 1076 } 1077 } 1078 1079 return $tags; 1080} 1081 1082function parseFunctionLike( 1083 PrettyPrinterAbstract $prettyPrinter, 1084 FunctionOrMethodName $name, 1085 int $classFlags, 1086 int $flags, 1087 Node\FunctionLike $func, 1088 ?string $cond 1089): FuncInfo { 1090 $comment = $func->getDocComment(); 1091 $paramMeta = []; 1092 $aliasType = null; 1093 $alias = null; 1094 $isDeprecated = false; 1095 $verify = true; 1096 $docReturnType = null; 1097 $docParamTypes = []; 1098 1099 if ($comment) { 1100 $tags = parseDocComment($comment); 1101 foreach ($tags as $tag) { 1102 if ($tag->name === 'prefer-ref') { 1103 $varName = $tag->getVariableName(); 1104 if (!isset($paramMeta[$varName])) { 1105 $paramMeta[$varName] = []; 1106 } 1107 $paramMeta[$varName]['preferRef'] = true; 1108 } else if ($tag->name === 'alias' || $tag->name === 'implementation-alias') { 1109 $aliasType = $tag->name; 1110 $aliasParts = explode("::", $tag->getValue()); 1111 if (count($aliasParts) === 1) { 1112 $alias = new FunctionName(new Name($aliasParts[0])); 1113 } else { 1114 $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]); 1115 } 1116 } else if ($tag->name === 'deprecated') { 1117 $isDeprecated = true; 1118 } else if ($tag->name === 'no-verify') { 1119 $verify = false; 1120 } else if ($tag->name === 'return') { 1121 $docReturnType = $tag->getType(); 1122 } else if ($tag->name === 'param') { 1123 $docParamTypes[$tag->getVariableName()] = $tag->getType(); 1124 } 1125 } 1126 } 1127 1128 $varNameSet = []; 1129 $args = []; 1130 $numRequiredArgs = 0; 1131 $foundVariadic = false; 1132 foreach ($func->getParams() as $i => $param) { 1133 $varName = $param->var->name; 1134 $preferRef = !empty($paramMeta[$varName]['preferRef']); 1135 unset($paramMeta[$varName]); 1136 1137 if (isset($varNameSet[$varName])) { 1138 throw new Exception("Duplicate parameter name $varName for function $name"); 1139 } 1140 $varNameSet[$varName] = true; 1141 1142 if ($preferRef) { 1143 $sendBy = ArgInfo::SEND_PREFER_REF; 1144 } else if ($param->byRef) { 1145 $sendBy = ArgInfo::SEND_BY_REF; 1146 } else { 1147 $sendBy = ArgInfo::SEND_BY_VAL; 1148 } 1149 1150 if ($foundVariadic) { 1151 throw new Exception("Error in function $name: only the last parameter can be variadic"); 1152 } 1153 1154 $type = $param->type ? Type::fromNode($param->type) : null; 1155 if ($type === null && !isset($docParamTypes[$varName])) { 1156 throw new Exception("Missing parameter type for function $name()"); 1157 } 1158 1159 if ($param->default instanceof Expr\ConstFetch && 1160 $param->default->name->toLowerString() === "null" && 1161 $type && !$type->isNullable() 1162 ) { 1163 $simpleType = $type->tryToSimpleType(); 1164 if ($simpleType === null) { 1165 throw new Exception( 1166 "Parameter $varName of function $name has null default, but is not nullable"); 1167 } 1168 } 1169 1170 if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") { 1171 throw new Exception('The exact class name must be used instead of "self"'); 1172 } 1173 1174 $foundVariadic = $param->variadic; 1175 1176 $args[] = new ArgInfo( 1177 $varName, 1178 $sendBy, 1179 $param->variadic, 1180 $type, 1181 isset($docParamTypes[$varName]) ? Type::fromPhpDoc($docParamTypes[$varName]) : null, 1182 $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null 1183 ); 1184 if (!$param->default && !$param->variadic) { 1185 $numRequiredArgs = $i + 1; 1186 } 1187 } 1188 1189 foreach (array_keys($paramMeta) as $var) { 1190 throw new Exception("Found metadata for invalid param $var of function $name"); 1191 } 1192 1193 $returnType = $func->getReturnType(); 1194 if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { 1195 throw new Exception("Missing return type for function $name()"); 1196 } 1197 1198 $return = new ReturnInfo( 1199 $func->returnsByRef(), 1200 $returnType ? Type::fromNode($returnType) : null, 1201 $docReturnType ? Type::fromPhpDoc($docReturnType) : null 1202 ); 1203 1204 return new FuncInfo( 1205 $name, 1206 $classFlags, 1207 $flags, 1208 $aliasType, 1209 $alias, 1210 $isDeprecated, 1211 $verify, 1212 $args, 1213 $return, 1214 $numRequiredArgs, 1215 $cond 1216 ); 1217} 1218 1219function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string { 1220 foreach ($stmt->getComments() as $comment) { 1221 $text = trim($comment->getText()); 1222 if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) { 1223 $conds[] = $matches[1]; 1224 } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) { 1225 $conds[] = "defined($matches[1])"; 1226 } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) { 1227 $conds[] = "!defined($matches[1])"; 1228 } else if (preg_match('/^#\s*else$/', $text)) { 1229 if (empty($conds)) { 1230 throw new Exception("Encountered else without corresponding #if"); 1231 } 1232 $cond = array_pop($conds); 1233 $conds[] = "!($cond)"; 1234 } else if (preg_match('/^#\s*endif$/', $text)) { 1235 if (empty($conds)) { 1236 throw new Exception("Encountered #endif without corresponding #if"); 1237 } 1238 array_pop($conds); 1239 } else if ($text[0] === '#') { 1240 throw new Exception("Unrecognized preprocessor directive \"$text\""); 1241 } 1242 } 1243 1244 return empty($conds) ? null : implode(' && ', $conds); 1245} 1246 1247function getFileDocComment(array $stmts): ?DocComment { 1248 if (empty($stmts)) { 1249 return null; 1250 } 1251 1252 $comments = $stmts[0]->getComments(); 1253 if (empty($comments)) { 1254 return null; 1255 } 1256 1257 if ($comments[0] instanceof DocComment) { 1258 return $comments[0]; 1259 } 1260 1261 return null; 1262} 1263 1264function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) { 1265 $conds = []; 1266 foreach ($stmts as $stmt) { 1267 if ($stmt instanceof Stmt\Nop) { 1268 continue; 1269 } 1270 1271 if ($stmt instanceof Stmt\Namespace_) { 1272 handleStatements($fileInfo, $stmt->stmts, $prettyPrinter); 1273 continue; 1274 } 1275 1276 $cond = handlePreprocessorConditions($conds, $stmt); 1277 if ($stmt instanceof Stmt\Function_) { 1278 $fileInfo->funcInfos[] = parseFunctionLike( 1279 $prettyPrinter, 1280 new FunctionName($stmt->namespacedName), 1281 0, 1282 0, 1283 $stmt, 1284 $cond 1285 ); 1286 continue; 1287 } 1288 1289 if ($stmt instanceof Stmt\ClassLike) { 1290 $className = $stmt->namespacedName; 1291 $methodInfos = []; 1292 foreach ($stmt->stmts as $classStmt) { 1293 $cond = handlePreprocessorConditions($conds, $classStmt); 1294 if ($classStmt instanceof Stmt\Nop) { 1295 continue; 1296 } 1297 1298 if (!$classStmt instanceof Stmt\ClassMethod) { 1299 throw new Exception("Not implemented {$classStmt->getType()}"); 1300 } 1301 1302 $classFlags = 0; 1303 if ($stmt instanceof Class_) { 1304 $classFlags = $stmt->flags; 1305 } 1306 1307 $flags = $classStmt->flags; 1308 if ($stmt instanceof Stmt\Interface_) { 1309 $flags |= Class_::MODIFIER_ABSTRACT; 1310 } 1311 1312 if (!($flags & Class_::VISIBILITY_MODIFIER_MASK)) { 1313 throw new Exception("Method visibility modifier is required"); 1314 } 1315 1316 $methodInfos[] = parseFunctionLike( 1317 $prettyPrinter, 1318 new MethodName($className, $classStmt->name->toString()), 1319 $classFlags, 1320 $flags, 1321 $classStmt, 1322 $cond 1323 ); 1324 } 1325 1326 $fileInfo->classInfos[] = new ClassInfo($className, $methodInfos); 1327 continue; 1328 } 1329 1330 throw new Exception("Unexpected node {$stmt->getType()}"); 1331 } 1332} 1333 1334function parseStubFile(string $code): FileInfo { 1335 $lexer = new PhpParser\Lexer(); 1336 $parser = new PhpParser\Parser\Php7($lexer); 1337 $nodeTraverser = new PhpParser\NodeTraverser; 1338 $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); 1339 $prettyPrinter = new class extends Standard { 1340 protected function pName_FullyQualified(Name\FullyQualified $node) { 1341 return implode('\\', $node->parts); 1342 } 1343 }; 1344 1345 $stmts = $parser->parse($code); 1346 $nodeTraverser->traverse($stmts); 1347 1348 $fileInfo = new FileInfo; 1349 $fileDocComment = getFileDocComment($stmts); 1350 if ($fileDocComment) { 1351 $fileTags = parseDocComment($fileDocComment); 1352 foreach ($fileTags as $tag) { 1353 if ($tag->name === 'generate-function-entries') { 1354 $fileInfo->generateFunctionEntries = true; 1355 $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : ""; 1356 } else if ($tag->name === 'generate-legacy-arginfo') { 1357 $fileInfo->generateLegacyArginfo = true; 1358 } 1359 } 1360 } 1361 1362 handleStatements($fileInfo, $stmts, $prettyPrinter); 1363 return $fileInfo; 1364} 1365 1366function funcInfoToCode(FuncInfo $funcInfo): string { 1367 $code = ''; 1368 $returnType = $funcInfo->return->type; 1369 if ($returnType !== null) { 1370 if (null !== $simpleReturnType = $returnType->tryToSimpleType()) { 1371 if ($simpleReturnType->isBuiltin) { 1372 $code .= sprintf( 1373 "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(%s, %d, %d, %s, %d)\n", 1374 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 1375 $funcInfo->numRequiredArgs, 1376 $simpleReturnType->toTypeCode(), $returnType->isNullable() 1377 ); 1378 } else { 1379 $code .= sprintf( 1380 "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(%s, %d, %d, %s, %d)\n", 1381 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 1382 $funcInfo->numRequiredArgs, 1383 $simpleReturnType->toEscapedName(), $returnType->isNullable() 1384 ); 1385 } 1386 } else { 1387 $arginfoType = $returnType->toArginfoType(); 1388 if ($arginfoType->hasClassType()) { 1389 $code .= sprintf( 1390 "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(%s, %d, %d, %s, %s)\n", 1391 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 1392 $funcInfo->numRequiredArgs, 1393 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask() 1394 ); 1395 } else { 1396 $code .= sprintf( 1397 "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(%s, %d, %d, %s)\n", 1398 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, 1399 $funcInfo->numRequiredArgs, 1400 $arginfoType->toTypeMask() 1401 ); 1402 } 1403 } 1404 } else { 1405 $code .= sprintf( 1406 "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n", 1407 $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs 1408 ); 1409 } 1410 1411 foreach ($funcInfo->args as $argInfo) { 1412 $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG"; 1413 $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : ""; 1414 $argType = $argInfo->type; 1415 if ($argType !== null) { 1416 if (null !== $simpleArgType = $argType->tryToSimpleType()) { 1417 if ($simpleArgType->isBuiltin) { 1418 $code .= sprintf( 1419 "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n", 1420 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 1421 $simpleArgType->toTypeCode(), $argType->isNullable(), 1422 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 1423 ); 1424 } else { 1425 $code .= sprintf( 1426 "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n", 1427 $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 1428 $simpleArgType->toEscapedName(), $argType->isNullable(), 1429 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 1430 ); 1431 } 1432 } else { 1433 $arginfoType = $argType->toArginfoType(); 1434 if ($arginfoType->hasClassType()) { 1435 $code .= sprintf( 1436 "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s, %s)\n", 1437 $argKind, $argInfo->getSendByString(), $argInfo->name, 1438 $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(), 1439 $argInfo->getDefaultValueAsArginfoString() 1440 ); 1441 } else { 1442 $code .= sprintf( 1443 "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n", 1444 $argKind, $argInfo->getSendByString(), $argInfo->name, 1445 $arginfoType->toTypeMask(), 1446 $argInfo->getDefaultValueAsArginfoString() 1447 ); 1448 } 1449 } 1450 } else { 1451 $code .= sprintf( 1452 "\tZEND_%s_INFO%s(%s, %s%s)\n", 1453 $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name, 1454 $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : "" 1455 ); 1456 } 1457 } 1458 1459 $code .= "ZEND_END_ARG_INFO()"; 1460 return $code . "\n"; 1461} 1462 1463/** @param FuncInfo[] $generatedFuncInfos */ 1464function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo { 1465 foreach ($generatedFuncInfos as $generatedFuncInfo) { 1466 if ($generatedFuncInfo->equalsApartFromName($funcInfo)) { 1467 return $generatedFuncInfo; 1468 } 1469 } 1470 return null; 1471} 1472 1473/** @param iterable<FuncInfo> $funcInfos */ 1474function generateCodeWithConditions( 1475 iterable $funcInfos, string $separator, Closure $codeGenerator): string { 1476 $code = ""; 1477 foreach ($funcInfos as $funcInfo) { 1478 $funcCode = $codeGenerator($funcInfo); 1479 if ($funcCode === null) { 1480 continue; 1481 } 1482 1483 $code .= $separator; 1484 if ($funcInfo->cond) { 1485 $code .= "#if {$funcInfo->cond}\n"; 1486 $code .= $funcCode; 1487 $code .= "#endif\n"; 1488 } else { 1489 $code .= $funcCode; 1490 } 1491 } 1492 return $code; 1493} 1494 1495function generateArgInfoCode(FileInfo $fileInfo, string $stubHash): string { 1496 $code = "/* This is a generated file, edit the .stub.php file instead.\n" 1497 . " * Stub hash: $stubHash */\n"; 1498 $generatedFuncInfos = []; 1499 $code .= generateCodeWithConditions( 1500 $fileInfo->getAllFuncInfos(), "\n", 1501 function (FuncInfo $funcInfo) use(&$generatedFuncInfos) { 1502 /* If there already is an equivalent arginfo structure, only emit a #define */ 1503 if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) { 1504 $code = sprintf( 1505 "#define %s %s\n", 1506 $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName() 1507 ); 1508 } else { 1509 $code = funcInfoToCode($funcInfo); 1510 } 1511 1512 $generatedFuncInfos[] = $funcInfo; 1513 return $code; 1514 } 1515 ); 1516 1517 if ($fileInfo->generateFunctionEntries) { 1518 $code .= "\n\n"; 1519 1520 $generatedFunctionDeclarations = []; 1521 $code .= generateCodeWithConditions( 1522 $fileInfo->getAllFuncInfos(), "", 1523 function (FuncInfo $funcInfo) use($fileInfo, &$generatedFunctionDeclarations) { 1524 $key = $funcInfo->getDeclarationKey(); 1525 if (isset($generatedFunctionDeclarations[$key])) { 1526 return null; 1527 } 1528 1529 $generatedFunctionDeclarations[$key] = true; 1530 return $fileInfo->declarationPrefix . $funcInfo->getDeclaration(); 1531 } 1532 ); 1533 1534 if (!empty($fileInfo->funcInfos)) { 1535 $code .= generateFunctionEntries(null, $fileInfo->funcInfos); 1536 } 1537 1538 foreach ($fileInfo->classInfos as $classInfo) { 1539 $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos); 1540 } 1541 } 1542 1543 return $code; 1544} 1545 1546/** @param FuncInfo[] $funcInfos */ 1547function generateFunctionEntries(?Name $className, array $funcInfos): string { 1548 $code = ""; 1549 1550 $functionEntryName = "ext_functions"; 1551 if ($className) { 1552 $underscoreName = implode("_", $className->parts); 1553 $functionEntryName = "class_{$underscoreName}_methods"; 1554 } 1555 1556 $code .= "\n\nstatic const zend_function_entry {$functionEntryName}[] = {\n"; 1557 $code .= generateCodeWithConditions($funcInfos, "", function (FuncInfo $funcInfo) { 1558 return $funcInfo->getFunctionEntry(); 1559 }); 1560 $code .= "\tZEND_FE_END\n"; 1561 $code .= "};\n"; 1562 1563 return $code; 1564} 1565 1566/** 1567 * @param FuncInfo[] $funcMap 1568 * @param FuncInfo[] $aliasMap 1569 * @return array<string, string> 1570 */ 1571function generateMethodSynopses(array $funcMap, array $aliasMap): array { 1572 $result = []; 1573 1574 foreach ($funcMap as $funcInfo) { 1575 $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap); 1576 if ($methodSynopsis !== null) { 1577 $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis; 1578 } 1579 } 1580 1581 return $result; 1582} 1583 1584/** 1585 * @param FuncInfo[] $funcMap 1586 * @param FuncInfo[] $aliasMap 1587 * @return array<string, string> 1588 */ 1589function replaceMethodSynopses(string $targetDirectory, array $funcMap, array $aliasMap): array { 1590 $methodSynopses = []; 1591 1592 $it = new RecursiveIteratorIterator( 1593 new RecursiveDirectoryIterator($targetDirectory), 1594 RecursiveIteratorIterator::LEAVES_ONLY 1595 ); 1596 1597 foreach ($it as $file) { 1598 $pathName = $file->getPathName(); 1599 if (!preg_match('/\.xml$/i', $pathName)) { 1600 continue; 1601 } 1602 1603 $xml = file_get_contents($pathName); 1604 if ($xml === false) { 1605 continue; 1606 } 1607 1608 if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) { 1609 continue; 1610 } 1611 1612 $replacedXml = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml); 1613 1614 $doc = new DOMDocument(); 1615 $doc->formatOutput = false; 1616 $doc->preserveWhiteSpace = true; 1617 $doc->validateOnParse = true; 1618 $success = $doc->loadXML($replacedXml); 1619 if (!$success) { 1620 echo "Failed opening $pathName\n"; 1621 continue; 1622 } 1623 1624 $docComparator = new DOMDocument(); 1625 $docComparator->preserveWhiteSpace = false; 1626 $docComparator->formatOutput = true; 1627 1628 $methodSynopsisElements = []; 1629 foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) { 1630 $methodSynopsisElements[] = $element; 1631 } 1632 foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) { 1633 $methodSynopsisElements[] = $element; 1634 } 1635 foreach ($doc->getElementsByTagName("methodsynopsis") as $element) { 1636 $methodSynopsisElements[] = $element; 1637 } 1638 1639 foreach ($methodSynopsisElements as $methodSynopsis) { 1640 if (!$methodSynopsis instanceof DOMElement) { 1641 continue; 1642 } 1643 1644 $list = $methodSynopsis->getElementsByTagName("methodname"); 1645 $item = $list->item(0); 1646 if (!$item instanceof DOMElement) { 1647 continue; 1648 } 1649 $funcName = $item->textContent; 1650 if (!isset($funcMap[$funcName])) { 1651 continue; 1652 } 1653 $funcInfo = $funcMap[$funcName]; 1654 1655 $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc); 1656 if ($newMethodSynopsis === null) { 1657 continue; 1658 } 1659 1660 // Retrieve current signature 1661 1662 $params = []; 1663 $list = $methodSynopsis->getElementsByTagName("methodparam"); 1664 foreach ($list as $i => $item) { 1665 if (!$item instanceof DOMElement) { 1666 continue; 1667 } 1668 1669 $paramList = $item->getElementsByTagName("parameter"); 1670 if ($paramList->count() !== 1) { 1671 continue; 1672 } 1673 1674 $paramName = $paramList->item(0)->textContent; 1675 $paramTypes = []; 1676 1677 $paramList = $item->getElementsByTagName("type"); 1678 foreach ($paramList as $type) { 1679 if (!$type instanceof DOMElement) { 1680 continue; 1681 } 1682 1683 $paramTypes[] = $type->textContent; 1684 } 1685 1686 $params[$paramName] = ["index" => $i, "type" => $paramTypes]; 1687 } 1688 1689 // Check if there is any change - short circuit if there is not any. 1690 1691 $xml1 = $doc->saveXML($methodSynopsis); 1692 $xml1 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml1); 1693 $docComparator->loadXML($xml1); 1694 $xml1 = $docComparator->saveXML(); 1695 1696 $methodSynopsis->parentNode->replaceChild($newMethodSynopsis, $methodSynopsis); 1697 1698 $xml2 = $doc->saveXML($newMethodSynopsis); 1699 $xml2 = preg_replace("/&([A-Za-z0-9._{}%-]+?;)/", "REPLACED-ENTITY-$1", $xml2); 1700 $docComparator->loadXML($xml2); 1701 $xml2 = $docComparator->saveXML(); 1702 1703 if ($xml1 === $xml2) { 1704 continue; 1705 } 1706 1707 // Update parameter references 1708 1709 $paramList = $doc->getElementsByTagName("parameter"); 1710 /** @var DOMElement $paramElement */ 1711 foreach ($paramList as $paramElement) { 1712 if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") { 1713 continue; 1714 } 1715 1716 $name = $paramElement->textContent; 1717 if (!isset($params[$name])) { 1718 continue; 1719 } 1720 1721 $index = $params[$name]["index"]; 1722 if (!isset($funcInfo->args[$index])) { 1723 continue; 1724 } 1725 1726 $paramElement->textContent = $funcInfo->args[$index]->name; 1727 } 1728 1729 // Return the updated XML 1730 1731 $replacedXml = $doc->saveXML(); 1732 1733 $replacedXml = preg_replace( 1734 [ 1735 "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/", 1736 "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 1737 "/<refentry\s+xmlns=\"([a-z0-9.:\/]+)\"\s+xmlns:xlink=\"([a-z0-9.:\/]+)\"\s+xml:id=\"([a-z0-9._-]+)\"\s*>/i", 1738 ], 1739 [ 1740 "&$1", 1741 "<refentry xml:id=\"$2\" xmlns=\"$1\">", 1742 "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">", 1743 ], 1744 $replacedXml 1745 ); 1746 1747 $methodSynopses[$pathName] = $replacedXml; 1748 } 1749 } 1750 1751 return $methodSynopses; 1752} 1753 1754function installPhpParser(string $version, string $phpParserDir) { 1755 $lockFile = __DIR__ . "/PHP-Parser-install-lock"; 1756 $lockFd = fopen($lockFile, 'w+'); 1757 if (!flock($lockFd, LOCK_EX)) { 1758 throw new Exception("Failed to acquire installation lock"); 1759 } 1760 1761 try { 1762 // Check whether a parallel process has already installed PHP-Parser. 1763 if (is_dir($phpParserDir)) { 1764 return; 1765 } 1766 1767 $cwd = getcwd(); 1768 chdir(__DIR__); 1769 1770 $tarName = "v$version.tar.gz"; 1771 passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 1772 if ($exit !== 0) { 1773 passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit); 1774 } 1775 if ($exit !== 0) { 1776 throw new Exception("Failed to download PHP-Parser tarball"); 1777 } 1778 if (!mkdir($phpParserDir)) { 1779 throw new Exception("Failed to create directory $phpParserDir"); 1780 } 1781 passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit); 1782 if ($exit !== 0) { 1783 throw new Exception("Failed to extract PHP-Parser tarball"); 1784 } 1785 unlink(__DIR__ . "/$tarName"); 1786 chdir($cwd); 1787 } finally { 1788 flock($lockFd, LOCK_UN); 1789 @unlink($lockFile); 1790 } 1791} 1792 1793function initPhpParser() { 1794 static $isInitialized = false; 1795 if ($isInitialized) { 1796 return; 1797 } 1798 1799 if (!extension_loaded("tokenizer")) { 1800 throw new Exception("The \"tokenizer\" extension is not available"); 1801 } 1802 1803 $isInitialized = true; 1804 $version = "4.13.0"; 1805 $phpParserDir = __DIR__ . "/PHP-Parser-$version"; 1806 if (!is_dir($phpParserDir)) { 1807 installPhpParser($version, $phpParserDir); 1808 } 1809 1810 spl_autoload_register(function(string $class) use($phpParserDir) { 1811 if (strpos($class, "PhpParser\\") === 0) { 1812 $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php"; 1813 require $fileName; 1814 } 1815 }); 1816} 1817 1818$optind = null; 1819$options = getopt("fh", ["force-regeneration", "parameter-stats", "help", "verify", "generate-methodsynopses", "replace-methodsynopses"], $optind); 1820 1821$context = new Context; 1822$printParameterStats = isset($options["parameter-stats"]); 1823$verify = isset($options["verify"]); 1824$generateMethodSynopses = isset($options["generate-methodsynopses"]); 1825$replaceMethodSynopses = isset($options["replace-methodsynopses"]); 1826$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]); 1827$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $generateMethodSynopses || $replaceMethodSynopses; 1828$targetMethodSynopses = $argv[$optind + 1] ?? null; 1829if ($replaceMethodSynopses && $targetMethodSynopses === null) { 1830 die("A target directory must be provided.\n"); 1831} 1832 1833if (isset($options["h"]) || isset($options["help"])) { 1834 die("\nusage: gen-stub.php [ -f | --force-regeneration ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n"); 1835} 1836 1837$fileInfos = []; 1838$location = $argv[$optind] ?? "."; 1839if (is_file($location)) { 1840 // Generate single file. 1841 $fileInfo = processStubFile($location, $context); 1842 if ($fileInfo) { 1843 $fileInfos[] = $fileInfo; 1844 } 1845} else if (is_dir($location)) { 1846 $fileInfos = processDirectory($location, $context); 1847} else { 1848 echo "$location is neither a file nor a directory.\n"; 1849 exit(1); 1850} 1851 1852if ($printParameterStats) { 1853 $parameterStats = []; 1854 1855 foreach ($fileInfos as $fileInfo) { 1856 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 1857 foreach ($funcInfo->args as $argInfo) { 1858 if (!isset($parameterStats[$argInfo->name])) { 1859 $parameterStats[$argInfo->name] = 0; 1860 } 1861 $parameterStats[$argInfo->name]++; 1862 } 1863 } 1864 } 1865 1866 arsort($parameterStats); 1867 echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n"; 1868} 1869 1870/** @var FuncInfo[] $funcMap */ 1871$funcMap = []; 1872/** @var FuncInfo[] $aliasMap */ 1873$aliasMap = []; 1874 1875foreach ($fileInfos as $fileInfo) { 1876 foreach ($fileInfo->getAllFuncInfos() as $funcInfo) { 1877 /** @var FuncInfo $funcInfo */ 1878 $funcMap[$funcInfo->name->__toString()] = $funcInfo; 1879 1880 if ($funcInfo->aliasType === "alias") { 1881 $aliasMap[$funcInfo->alias->__toString()] = $funcInfo; 1882 } 1883 } 1884} 1885 1886if ($verify) { 1887 $errors = []; 1888 1889 foreach ($aliasMap as $aliasFunc) { 1890 if (!isset($funcMap[$aliasFunc->alias->__toString()])) { 1891 $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found"; 1892 continue; 1893 } 1894 1895 if (!$aliasFunc->verify) { 1896 continue; 1897 } 1898 1899 $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()]; 1900 $aliasedArgs = $aliasedFunc->args; 1901 $aliasArgs = $aliasFunc->args; 1902 1903 if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) { 1904 if ($aliasFunc->isInstanceMethod()) { 1905 $aliasedArgs = array_slice($aliasedArgs, 1); 1906 } 1907 1908 if ($aliasedFunc->isInstanceMethod()) { 1909 $aliasArgs = array_slice($aliasArgs, 1); 1910 } 1911 } 1912 1913 array_map( 1914 function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) { 1915 if ($aliasArg === null) { 1916 assert($aliasedArg !== null); 1917 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing"; 1918 return null; 1919 } 1920 1921 if ($aliasedArg === null) { 1922 $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing"; 1923 return null; 1924 } 1925 1926 if ($aliasArg->name !== $aliasedArg->name) { 1927 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name"; 1928 return null; 1929 } 1930 1931 if ($aliasArg->type != $aliasedArg->type) { 1932 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type"; 1933 } 1934 1935 if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) { 1936 $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value"; 1937 } 1938 }, 1939 $aliasArgs, $aliasedArgs 1940 ); 1941 1942 if ((!$aliasedFunc->isMethod() || $aliasedFunc->isFinalMethod()) && 1943 (!$aliasFunc->isMethod() || $aliasFunc->isFinalMethod()) && 1944 $aliasFunc->return != $aliasedFunc->return 1945 ) { 1946 $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type"; 1947 } 1948 } 1949 1950 echo implode("\n", $errors); 1951 if (!empty($errors)) { 1952 echo "\n"; 1953 exit(1); 1954 } 1955} 1956 1957if ($generateMethodSynopses) { 1958 $methodSynopsesDirectory = getcwd() . "/methodsynopses"; 1959 1960 $methodSynopses = generateMethodSynopses($funcMap, $aliasMap); 1961 if (!empty($methodSynopses)) { 1962 if (!file_exists($methodSynopsesDirectory)) { 1963 mkdir($methodSynopsesDirectory); 1964 } 1965 1966 foreach ($methodSynopses as $filename => $content) { 1967 if (file_put_contents("$methodSynopsesDirectory/$filename", $content)) { 1968 echo "Saved $filename\n"; 1969 } 1970 } 1971 } 1972} 1973 1974if ($replaceMethodSynopses) { 1975 $methodSynopses = replaceMethodSynopses($targetMethodSynopses, $funcMap, $aliasMap); 1976 1977 foreach ($methodSynopses as $filename => $content) { 1978 if (file_put_contents($filename, $content)) { 1979 echo "Saved $filename\n"; 1980 } 1981 } 1982} 1983