xref: /PHP-8.0/build/gen_stub.php (revision 36de002c)
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