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