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