xref: /php-src/build/gen_stub.php (revision b06f2bc6)
1#!/usr/bin/env php
2<?php declare(strict_types=1);
3
4use PhpParser\Comment\Doc as DocComment;
5use PhpParser\ConstExprEvaluator;
6use PhpParser\Modifiers;
7use PhpParser\Node;
8use PhpParser\Node\AttributeGroup;
9use PhpParser\Node\Expr;
10use PhpParser\Node\Name;
11use PhpParser\Node\Stmt;
12use PhpParser\Node\Stmt\Class_;
13use PhpParser\Node\Stmt\Enum_;
14use PhpParser\Node\Stmt\Interface_;
15use PhpParser\Node\Stmt\Trait_;
16use PhpParser\PrettyPrinter\Standard;
17use PhpParser\PrettyPrinterAbstract;
18
19error_reporting(E_ALL);
20ini_set("precision", "-1");
21
22const PHP_70_VERSION_ID = 70000;
23const PHP_80_VERSION_ID = 80000;
24const PHP_81_VERSION_ID = 80100;
25const PHP_82_VERSION_ID = 80200;
26const PHP_83_VERSION_ID = 80300;
27const PHP_84_VERSION_ID = 80400;
28const ALL_PHP_VERSION_IDS = [
29    PHP_70_VERSION_ID,
30    PHP_80_VERSION_ID,
31    PHP_81_VERSION_ID,
32    PHP_82_VERSION_ID,
33    PHP_83_VERSION_ID,
34    PHP_84_VERSION_ID,
35];
36
37/**
38 * @return FileInfo[]
39 */
40function processDirectory(string $dir, Context $context): array {
41    $pathNames = [];
42    $it = new RecursiveIteratorIterator(
43        new RecursiveDirectoryIterator($dir),
44        RecursiveIteratorIterator::LEAVES_ONLY
45    );
46    foreach ($it as $file) {
47        $pathName = $file->getPathName();
48        if (preg_match('/\.stub\.php$/', $pathName)) {
49            $pathNames[] = $pathName;
50        }
51    }
52
53    // Make sure stub files are processed in a predictable, system-independent order.
54    sort($pathNames);
55
56    $fileInfos = [];
57    foreach ($pathNames as $pathName) {
58        $fileInfo = processStubFile($pathName, $context);
59        if ($fileInfo) {
60            $fileInfos[] = $fileInfo;
61        }
62    }
63    return $fileInfos;
64}
65
66function processStubFile(string $stubFile, Context $context, bool $includeOnly = false): ?FileInfo {
67    try {
68        if (!file_exists($stubFile)) {
69            throw new Exception("File $stubFile does not exist");
70        }
71
72        if (!$includeOnly) {
73            $stubFilenameWithoutExtension = str_replace(".stub.php", "", $stubFile);
74            $arginfoFile = "{$stubFilenameWithoutExtension}_arginfo.h";
75            $legacyFile = "{$stubFilenameWithoutExtension}_legacy_arginfo.h";
76
77            $stubCode = file_get_contents($stubFile);
78            $stubHash = computeStubHash($stubCode);
79            $oldStubHash = extractStubHash($arginfoFile);
80            if ($stubHash === $oldStubHash && !$context->forceParse) {
81                /* Stub file did not change, do not regenerate. */
82                return null;
83            }
84        }
85
86        if (!$fileInfo = $context->parsedFiles[$stubFile] ?? null) {
87            initPhpParser();
88            $stubContent = $stubCode ?? file_get_contents($stubFile);
89            $fileInfo = parseStubFile($stubContent);
90            $context->parsedFiles[$stubFile] = $fileInfo;
91
92            foreach ($fileInfo->dependencies as $dependency) {
93                // TODO add header search path for extensions?
94                $prefixes = [dirname($stubFile) . "/", dirname(__DIR__) . "/"];
95                foreach ($prefixes as $prefix) {
96                    $depFile = $prefix . $dependency;
97                    if (file_exists($depFile)) {
98                        break;
99                    }
100                    $depFile = null;
101                }
102                if (!$depFile) {
103                    throw new Exception("File $stubFile includes a file $dependency which does not exist");
104                }
105                processStubFile($depFile, $context, true);
106            }
107
108            $constInfos = $fileInfo->getAllConstInfos();
109            $context->allConstInfos = array_merge($context->allConstInfos, $constInfos);
110        }
111
112        if ($includeOnly) {
113            return $fileInfo;
114        }
115
116        $arginfoCode = generateArgInfoCode(
117            basename($stubFilenameWithoutExtension),
118            $fileInfo,
119            $context->allConstInfos,
120            $stubHash
121        );
122        if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($arginfoFile, $arginfoCode)) {
123            echo "Saved $arginfoFile\n";
124        }
125
126        if ($fileInfo->shouldGenerateLegacyArginfo()) {
127            $legacyFileInfo = clone $fileInfo;
128            $legacyFileInfo->legacyArginfoGeneration = true;
129            $phpVersionIdMinimumCompatibility = $legacyFileInfo->getMinimumPhpVersionIdCompatibility();
130
131            foreach ($legacyFileInfo->getAllFuncInfos() as $funcInfo) {
132                $funcInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
133            }
134            foreach ($legacyFileInfo->getAllClassInfos() as $classInfo) {
135                $classInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
136            }
137            foreach ($legacyFileInfo->getAllConstInfos() as $constInfo) {
138                $constInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
139            }
140
141            $arginfoCode = generateArgInfoCode(
142                basename($stubFilenameWithoutExtension),
143                $legacyFileInfo,
144                $context->allConstInfos,
145                $stubHash
146            );
147            if (($context->forceRegeneration || $stubHash !== $oldStubHash) && file_put_contents($legacyFile, $arginfoCode)) {
148                echo "Saved $legacyFile\n";
149            }
150        }
151
152        return $fileInfo;
153    } catch (Exception $e) {
154        echo "In $stubFile:\n{$e->getMessage()}\n";
155        exit(1);
156    }
157}
158
159function computeStubHash(string $stubCode): string {
160    return sha1(str_replace("\r\n", "\n", $stubCode));
161}
162
163function extractStubHash(string $arginfoFile): ?string {
164    if (!file_exists($arginfoFile)) {
165        return null;
166    }
167
168    $arginfoCode = file_get_contents($arginfoFile);
169    if (!preg_match('/\* Stub hash: ([0-9a-f]+) \*/', $arginfoCode, $matches)) {
170        return null;
171    }
172
173    return $matches[1];
174}
175
176class Context {
177    public bool $forceParse = false;
178    public bool $forceRegeneration = false;
179    /** @var array<string, ConstInfo> */
180    public array $allConstInfos = [];
181    /** @var FileInfo[] */
182    public array $parsedFiles = [];
183}
184
185class ArrayType extends SimpleType {
186    public Type $keyType;
187    public Type $valueType;
188
189    public static function createGenericArray(): self
190    {
191        return new ArrayType(Type::fromString("int|string"), Type::fromString("mixed|ref"));
192    }
193
194    public function __construct(Type $keyType, Type $valueType)
195    {
196        parent::__construct("array", true);
197
198        $this->keyType = $keyType;
199        $this->valueType = $valueType;
200    }
201
202    public function toOptimizerTypeMask(): string {
203        $typeMasks = [
204            parent::toOptimizerTypeMask(),
205            $this->keyType->toOptimizerTypeMaskForArrayKey(),
206            $this->valueType->toOptimizerTypeMaskForArrayValue(),
207        ];
208
209        return implode("|", $typeMasks);
210    }
211
212    public function equals(SimpleType $other): bool {
213        if (!parent::equals($other)) {
214            return false;
215        }
216
217        assert(get_class($other) === self::class);
218
219        return Type::equals($this->keyType, $other->keyType) &&
220            Type::equals($this->valueType, $other->valueType);
221    }
222}
223
224class SimpleType {
225    public string $name;
226    public bool $isBuiltin;
227
228    public static function fromNode(Node $node): SimpleType {
229        if ($node instanceof Node\Name) {
230            if ($node->toLowerString() === 'static') {
231                // PHP internally considers "static" a builtin type.
232                return new SimpleType($node->toLowerString(), true);
233            }
234
235            if ($node->toLowerString() === 'self') {
236                throw new Exception('The exact class name must be used instead of "self"');
237            }
238
239            assert($node->isFullyQualified());
240            return new SimpleType($node->toString(), false);
241        }
242
243        if ($node instanceof Node\Identifier) {
244            if ($node->toLowerString() === 'array') {
245                return ArrayType::createGenericArray();
246            }
247
248            return new SimpleType($node->toLowerString(), true);
249        }
250
251        throw new Exception("Unexpected node type");
252    }
253
254    public static function fromString(string $typeString): SimpleType
255    {
256        switch (strtolower($typeString)) {
257            case "void":
258            case "null":
259            case "false":
260            case "true":
261            case "bool":
262            case "int":
263            case "float":
264            case "string":
265            case "callable":
266            case "object":
267            case "resource":
268            case "mixed":
269            case "static":
270            case "never":
271            case "ref":
272                return new SimpleType(strtolower($typeString), true);
273            case "array":
274                return ArrayType::createGenericArray();
275            case "self":
276                throw new Exception('The exact class name must be used instead of "self"');
277            case "iterable":
278                throw new Exception('This should not happen');
279        }
280
281        $matches = [];
282        $isArray = preg_match("/(.*)\s*\[\s*\]/", $typeString, $matches);
283        if ($isArray) {
284            return new ArrayType(Type::fromString("int"), Type::fromString($matches[1]));
285        }
286
287        $matches = [];
288        $isArray = preg_match("/array\s*<\s*([A-Za-z0-9_|-]+)?(\s*,\s*)?([A-Za-z0-9_|-]+)?\s*>/i", $typeString, $matches);
289        if ($isArray) {
290            if (empty($matches[1]) || empty($matches[3])) {
291                throw new Exception("array<> type hint must have both a key and a value");
292            }
293
294            return new ArrayType(Type::fromString($matches[1]), Type::fromString($matches[3]));
295        }
296
297        return new SimpleType($typeString, false);
298    }
299
300    /**
301     * @param mixed $value
302     */
303    public static function fromValue($value): SimpleType
304    {
305        switch (gettype($value)) {
306            case "NULL":
307                return SimpleType::null();
308            case "boolean":
309                return SimpleType::bool();
310            case "integer":
311                return SimpleType::int();
312            case "double":
313                return SimpleType::float();
314            case "string":
315                return SimpleType::string();
316            case "array":
317                return SimpleType::array();
318            case "object":
319                return SimpleType::object();
320            default:
321                throw new Exception("Type \"" . gettype($value) . "\" cannot be inferred based on value");
322        }
323    }
324
325    public static function null(): SimpleType
326    {
327        return new SimpleType("null", true);
328    }
329
330    public static function bool(): SimpleType
331    {
332        return new SimpleType("bool", true);
333    }
334
335    public static function int(): SimpleType
336    {
337        return new SimpleType("int", true);
338    }
339
340    public static function float(): SimpleType
341    {
342        return new SimpleType("float", true);
343    }
344
345    public static function string(): SimpleType
346    {
347        return new SimpleType("string", true);
348    }
349
350    public static function array(): SimpleType
351    {
352        return new SimpleType("array", true);
353    }
354
355    public static function object(): SimpleType
356    {
357        return new SimpleType("object", true);
358    }
359
360    public static function void(): SimpleType
361    {
362        return new SimpleType("void", true);
363    }
364
365    protected function __construct(string $name, bool $isBuiltin) {
366        $this->name = $name;
367        $this->isBuiltin = $isBuiltin;
368    }
369
370    public function isScalar(): bool {
371        return $this->isBuiltin && in_array($this->name, ["null", "false", "true", "bool", "int", "float"], true);
372    }
373
374    public function isNull(): bool {
375        return $this->isBuiltin && $this->name === 'null';
376    }
377
378    public function isBool(): bool {
379        return $this->isBuiltin && $this->name === 'bool';
380    }
381
382    public function isInt(): bool {
383        return $this->isBuiltin && $this->name === 'int';
384    }
385
386    public function isFloat(): bool {
387        return $this->isBuiltin && $this->name === 'float';
388    }
389
390    public function isString(): bool {
391        return $this->isBuiltin && $this->name === 'string';
392    }
393
394    public function isArray(): bool {
395        return $this->isBuiltin && $this->name === 'array';
396    }
397
398    public function isMixed(): bool {
399        return $this->isBuiltin && $this->name === 'mixed';
400    }
401
402    public function toTypeCode(): string {
403        assert($this->isBuiltin);
404        switch ($this->name) {
405            case "bool":
406                return "_IS_BOOL";
407            case "int":
408                return "IS_LONG";
409            case "float":
410                return "IS_DOUBLE";
411            case "string":
412                return "IS_STRING";
413            case "array":
414                return "IS_ARRAY";
415            case "object":
416                return "IS_OBJECT";
417            case "void":
418                return "IS_VOID";
419            case "callable":
420                return "IS_CALLABLE";
421            case "mixed":
422                return "IS_MIXED";
423            case "static":
424                return "IS_STATIC";
425            case "never":
426                return "IS_NEVER";
427            case "null":
428                return "IS_NULL";
429            case "false":
430                return "IS_FALSE";
431            case "true":
432                return "IS_TRUE";
433            default:
434                throw new Exception("Not implemented: $this->name");
435        }
436    }
437
438    public function toTypeMask(): string {
439        assert($this->isBuiltin);
440
441        switch ($this->name) {
442            case "null":
443                return "MAY_BE_NULL";
444            case "false":
445                return "MAY_BE_FALSE";
446            case "true":
447                return "MAY_BE_TRUE";
448            case "bool":
449                return "MAY_BE_BOOL";
450            case "int":
451                return "MAY_BE_LONG";
452            case "float":
453                return "MAY_BE_DOUBLE";
454            case "string":
455                return "MAY_BE_STRING";
456            case "array":
457                return "MAY_BE_ARRAY";
458            case "object":
459                return "MAY_BE_OBJECT";
460            case "callable":
461                return "MAY_BE_CALLABLE";
462            case "mixed":
463                return "MAY_BE_ANY";
464            case "void":
465                return "MAY_BE_VOID";
466            case "static":
467                return "MAY_BE_STATIC";
468            case "never":
469                return "MAY_BE_NEVER";
470            default:
471                throw new Exception("Not implemented: $this->name");
472        }
473    }
474
475    public function toOptimizerTypeMaskForArrayKey(): string {
476        assert($this->isBuiltin);
477
478        switch ($this->name) {
479            case "int":
480                return "MAY_BE_ARRAY_KEY_LONG";
481            case "string":
482                return "MAY_BE_ARRAY_KEY_STRING";
483            default:
484                throw new Exception("Type $this->name cannot be an array key");
485        }
486    }
487
488    public function toOptimizerTypeMaskForArrayValue(): string {
489        if (!$this->isBuiltin) {
490            return "MAY_BE_ARRAY_OF_OBJECT";
491        }
492
493        switch ($this->name) {
494            case "null":
495                return "MAY_BE_ARRAY_OF_NULL";
496            case "false":
497                return "MAY_BE_ARRAY_OF_FALSE";
498            case "true":
499                return "MAY_BE_ARRAY_OF_TRUE";
500            case "bool":
501                return "MAY_BE_ARRAY_OF_FALSE|MAY_BE_ARRAY_OF_TRUE";
502            case "int":
503                return "MAY_BE_ARRAY_OF_LONG";
504            case "float":
505                return "MAY_BE_ARRAY_OF_DOUBLE";
506            case "string":
507                return "MAY_BE_ARRAY_OF_STRING";
508            case "array":
509                return "MAY_BE_ARRAY_OF_ARRAY";
510            case "object":
511                return "MAY_BE_ARRAY_OF_OBJECT";
512            case "resource":
513                return "MAY_BE_ARRAY_OF_RESOURCE";
514            case "mixed":
515                return "MAY_BE_ARRAY_OF_ANY";
516            case "ref":
517                return "MAY_BE_ARRAY_OF_REF";
518            default:
519                throw new Exception("Type $this->name cannot be an array value");
520        }
521    }
522
523    public function toOptimizerTypeMask(): string {
524        if (!$this->isBuiltin) {
525            return "MAY_BE_OBJECT";
526        }
527
528        switch ($this->name) {
529            case "resource":
530                return "MAY_BE_RESOURCE";
531            case "callable":
532                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";
533            case "iterable":
534                return "MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY|MAY_BE_OBJECT";
535            case "mixed":
536                return "MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY";
537        }
538
539        return $this->toTypeMask();
540    }
541
542    public function toEscapedName(): string {
543        // Escape backslashes, and also encode \u, \U, and \N to avoid compilation errors in generated macros
544        return str_replace(
545            ['\\', '\\u', '\\U', '\\N'],
546            ['\\\\', '\\\\165', '\\\\125', '\\\\116'],
547            $this->name
548        );
549    }
550
551    public function toVarEscapedName(): string {
552        return str_replace('\\', '_', $this->name);
553    }
554
555    public function equals(SimpleType $other): bool {
556        return $this->name === $other->name && $this->isBuiltin === $other->isBuiltin;
557    }
558}
559
560class Type {
561    /** @var SimpleType[] */
562    public array $types;
563    public bool $isIntersection;
564
565    public static function fromNode(Node $node): Type {
566        if ($node instanceof Node\UnionType || $node instanceof Node\IntersectionType) {
567            $nestedTypeObjects = array_map(['Type', 'fromNode'], $node->types);
568            $types = [];
569            foreach ($nestedTypeObjects as $typeObject) {
570                array_push($types, ...$typeObject->types);
571            }
572            return new Type($types, ($node instanceof Node\IntersectionType));
573        }
574
575        if ($node instanceof Node\NullableType) {
576            return new Type(
577                [
578                    ...Type::fromNode($node->type)->types,
579                    SimpleType::null(),
580                ],
581                false
582            );
583        }
584
585        if ($node instanceof Node\Identifier && $node->toLowerString() === "iterable") {
586            return new Type(
587                [
588                    SimpleType::fromString("Traversable"),
589                    ArrayType::createGenericArray(),
590                ],
591                false
592            );
593        }
594
595        return new Type([SimpleType::fromNode($node)], false);
596    }
597
598    public static function fromString(string $typeString): self {
599        $typeString .= "|";
600        $simpleTypes = [];
601        $simpleTypeOffset = 0;
602        $inArray = false;
603        $isIntersection = false;
604
605        $typeStringLength = strlen($typeString);
606        for ($i = 0; $i < $typeStringLength; $i++) {
607            $char = $typeString[$i];
608
609            if ($char === "<") {
610                $inArray = true;
611                continue;
612            }
613
614            if ($char === ">") {
615                $inArray = false;
616                continue;
617            }
618
619            if ($inArray) {
620                continue;
621            }
622
623            if ($char === "|" || $char === "&") {
624                $isIntersection = ($char === "&");
625                $simpleTypeName = trim(substr($typeString, $simpleTypeOffset, $i - $simpleTypeOffset));
626
627                $simpleTypes[] = SimpleType::fromString($simpleTypeName);
628
629                $simpleTypeOffset = $i + 1;
630            }
631        }
632
633        return new Type($simpleTypes, $isIntersection);
634    }
635
636    /**
637     * @param SimpleType[] $types
638     */
639    private function __construct(array $types, bool $isIntersection) {
640        $this->types = $types;
641        $this->isIntersection = $isIntersection;
642    }
643
644    public function isScalar(): bool {
645        foreach ($this->types as $type) {
646            if (!$type->isScalar()) {
647                return false;
648            }
649        }
650
651        return true;
652    }
653
654    public function isNullable(): bool {
655        foreach ($this->types as $type) {
656            if ($type->isNull()) {
657                return true;
658            }
659        }
660
661        return false;
662    }
663
664    public function getWithoutNull(): Type {
665        return new Type(
666            array_values(
667                array_filter(
668                    $this->types,
669                    function(SimpleType $type) {
670                        return !$type->isNull();
671                    }
672                )
673            ),
674            false
675        );
676    }
677
678    public function tryToSimpleType(): ?SimpleType {
679        $withoutNull = $this->getWithoutNull();
680        /* type has only null */
681        if (count($withoutNull->types) === 0) {
682            return $this->types[0];
683        }
684        if (count($withoutNull->types) === 1) {
685            return $withoutNull->types[0];
686        }
687        return null;
688    }
689
690    public function toArginfoType(): ArginfoType {
691        $classTypes = [];
692        $builtinTypes = [];
693        foreach ($this->types as $type) {
694            if ($type->isBuiltin) {
695                $builtinTypes[] = $type;
696            } else {
697                $classTypes[] = $type;
698            }
699        }
700        return new ArginfoType($classTypes, $builtinTypes);
701    }
702
703    public function toOptimizerTypeMask(): string {
704        $optimizerTypes = [];
705
706        foreach ($this->types as $type) {
707            // TODO Support for toOptimizerMask for intersection
708            $optimizerTypes[] = $type->toOptimizerTypeMask();
709        }
710
711        return implode("|", $optimizerTypes);
712    }
713
714    public function toOptimizerTypeMaskForArrayKey(): string {
715        $typeMasks = [];
716
717        foreach ($this->types as $type) {
718            $typeMasks[] = $type->toOptimizerTypeMaskForArrayKey();
719        }
720
721        return implode("|", $typeMasks);
722    }
723
724    public function toOptimizerTypeMaskForArrayValue(): string {
725        $typeMasks = [];
726
727        foreach ($this->types as $type) {
728            $typeMasks[] = $type->toOptimizerTypeMaskForArrayValue();
729        }
730
731        return implode("|", $typeMasks);
732    }
733
734    public function getTypeForDoc(DOMDocument $doc): DOMElement {
735        if (count($this->types) > 1) {
736            $typeSort = $this->isIntersection ? "intersection" : "union";
737            $typeElement = $doc->createElement('type');
738            $typeElement->setAttribute("class", $typeSort);
739
740            foreach ($this->types as $type) {
741                $unionTypeElement = $doc->createElement('type', $type->name);
742                $typeElement->appendChild($unionTypeElement);
743            }
744        } else {
745            $type = $this->types[0];
746            $name = $type->name;
747
748            $typeElement = $doc->createElement('type', $name);
749        }
750
751        return $typeElement;
752    }
753
754    public static function equals(?Type $a, ?Type $b): bool {
755        if ($a === null || $b === null) {
756            return $a === $b;
757        }
758
759        if (count($a->types) !== count($b->types)) {
760            return false;
761        }
762
763        for ($i = 0; $i < count($a->types); $i++) {
764            if (!$a->types[$i]->equals($b->types[$i])) {
765                return false;
766            }
767        }
768
769        return true;
770    }
771
772    public function __toString() {
773        if ($this->types === null) {
774            return 'mixed';
775        }
776
777        $char = $this->isIntersection ? '&' : '|';
778        return implode($char, array_map(
779            function ($type) { return $type->name; },
780            $this->types)
781        );
782    }
783}
784
785class ArginfoType {
786    /** @var SimpleType[] $classTypes */
787    public array $classTypes;
788    /** @var SimpleType[] $builtinTypes */
789    private array $builtinTypes;
790
791    /**
792     * @param SimpleType[] $classTypes
793     * @param SimpleType[] $builtinTypes
794     */
795    public function __construct(array $classTypes, array $builtinTypes) {
796        $this->classTypes = $classTypes;
797        $this->builtinTypes = $builtinTypes;
798    }
799
800    public function hasClassType(): bool {
801        return !empty($this->classTypes);
802    }
803
804    public function toClassTypeString(): string {
805        return implode('|', array_map(function(SimpleType $type) {
806            return $type->toEscapedName();
807        }, $this->classTypes));
808    }
809
810    public function toTypeMask(): string {
811        if (empty($this->builtinTypes)) {
812            return '0';
813        }
814        return implode('|', array_map(function(SimpleType $type) {
815            return $type->toTypeMask();
816        }, $this->builtinTypes));
817    }
818}
819
820class ArgInfo {
821    const SEND_BY_VAL = 0;
822    const SEND_BY_REF = 1;
823    const SEND_PREFER_REF = 2;
824
825    public string $name;
826    public int $sendBy;
827    public bool $isVariadic;
828    public ?Type $type;
829    public ?Type $phpDocType;
830    public ?string $defaultValue;
831    /** @var AttributeInfo[] */
832    public array $attributes;
833
834    /**
835     * @param AttributeInfo[] $attributes
836     */
837    public function __construct(
838        string $name,
839        int $sendBy,
840        bool $isVariadic,
841        ?Type $type,
842        ?Type $phpDocType,
843        ?string $defaultValue,
844        array $attributes
845    ) {
846        $this->name = $name;
847        $this->sendBy = $sendBy;
848        $this->isVariadic = $isVariadic;
849        $this->setTypes($type, $phpDocType);
850        $this->defaultValue = $defaultValue;
851        $this->attributes = $attributes;
852    }
853
854    public function equals(ArgInfo $other): bool {
855        return $this->name === $other->name
856            && $this->sendBy === $other->sendBy
857            && $this->isVariadic === $other->isVariadic
858            && Type::equals($this->type, $other->type)
859            && $this->defaultValue === $other->defaultValue;
860    }
861
862    public function getSendByString(): string {
863        switch ($this->sendBy) {
864        case self::SEND_BY_VAL:
865            return "0";
866        case self::SEND_BY_REF:
867            return "1";
868        case self::SEND_PREFER_REF:
869            return "ZEND_SEND_PREFER_REF";
870        }
871        throw new Exception("Invalid sendBy value");
872    }
873
874    public function getMethodSynopsisType(): Type {
875        if ($this->type) {
876            return $this->type;
877        }
878
879        if ($this->phpDocType) {
880            return $this->phpDocType;
881        }
882
883        throw new Exception("A parameter must have a type");
884    }
885
886    public function hasProperDefaultValue(): bool {
887        return $this->defaultValue !== null && $this->defaultValue !== "UNKNOWN";
888    }
889
890    public function getDefaultValueAsArginfoString(): string {
891        if ($this->hasProperDefaultValue()) {
892            return '"' . addslashes($this->defaultValue) . '"';
893        }
894
895        return "NULL";
896    }
897
898    public function getDefaultValueAsMethodSynopsisString(): ?string {
899        if ($this->defaultValue === null) {
900            return null;
901        }
902
903        switch ($this->defaultValue) {
904            case 'UNKNOWN':
905                return null;
906            case 'false':
907            case 'true':
908            case 'null':
909                return "&{$this->defaultValue};";
910        }
911
912        return $this->defaultValue;
913    }
914
915    private function setTypes(?Type $type, ?Type $phpDocType): void
916    {
917        $this->type = $type;
918        $this->phpDocType = $phpDocType;
919    }
920}
921
922interface VariableLikeName {
923    public function __toString(): string;
924    public function getDeclarationName(): string;
925}
926
927interface ConstOrClassConstName extends VariableLikeName {
928    public function equals(ConstOrClassConstName $const): bool;
929    public function isClassConst(): bool;
930    public function isUnknown(): bool;
931}
932
933abstract class AbstractConstName implements ConstOrClassConstName
934{
935    public function equals(ConstOrClassConstName $const): bool
936    {
937        return $this->__toString() === $const->__toString();
938    }
939
940    public function isUnknown(): bool
941    {
942        return strtolower($this->__toString()) === "unknown";
943    }
944}
945
946class ConstName extends AbstractConstName {
947    public string $const;
948
949    public function __construct(?Name $namespace, string $const)
950    {
951        if ($namespace && ($namespace = $namespace->slice(0, -1))) {
952            $const = $namespace->toString() . '\\' . $const;
953        }
954        $this->const = $const;
955    }
956
957    public function isClassConst(): bool
958    {
959        return false;
960    }
961
962    public function isUnknown(): bool
963    {
964        $name = $this->__toString();
965        if (($pos = strrpos($name, '\\')) !== false) {
966            $name = substr($name, $pos + 1);
967        }
968        return strtolower($name) === "unknown";
969    }
970
971    public function __toString(): string
972    {
973        return $this->const;
974    }
975
976    public function getDeclarationName(): string
977    {
978        return $this->name->toString();
979    }
980}
981
982class ClassConstName extends AbstractConstName {
983    public Name $class;
984    public string $const;
985
986    public function __construct(Name $class, string $const)
987    {
988        $this->class = $class;
989        $this->const = $const;
990    }
991
992    public function isClassConst(): bool
993    {
994        return true;
995    }
996
997    public function __toString(): string
998    {
999        return $this->class->toString() . "::" . $this->const;
1000    }
1001
1002    public function getDeclarationName(): string
1003    {
1004        return $this->const;
1005    }
1006}
1007
1008class PropertyName implements VariableLikeName {
1009    public Name $class;
1010    public string $property;
1011
1012    public function __construct(Name $class, string $property)
1013    {
1014        $this->class = $class;
1015        $this->property = $property;
1016    }
1017
1018    public function __toString(): string
1019    {
1020        return $this->class->toString() . "::$" . $this->property;
1021    }
1022
1023    public function getDeclarationName(): string
1024    {
1025         return $this->property;
1026    }
1027}
1028
1029interface FunctionOrMethodName {
1030    public function getDeclaration(): string;
1031    public function getArgInfoName(): string;
1032    public function getMethodSynopsisFilename(): string;
1033    public function getNameForAttributes(): string;
1034    public function __toString(): string;
1035    public function isMethod(): bool;
1036    public function isConstructor(): bool;
1037    public function isDestructor(): bool;
1038}
1039
1040class FunctionName implements FunctionOrMethodName {
1041    private Name $name;
1042
1043    public function __construct(Name $name) {
1044        $this->name = $name;
1045    }
1046
1047    public function getNamespace(): ?string {
1048        if ($this->name->isQualified()) {
1049            return $this->name->slice(0, -1)->toString();
1050        }
1051        return null;
1052    }
1053
1054    public function getNonNamespacedName(): string {
1055        if ($this->name->isQualified()) {
1056            throw new Exception("Namespaced name not supported here");
1057        }
1058        return $this->name->toString();
1059    }
1060
1061    public function getDeclarationName(): string {
1062        return implode('_', $this->name->getParts());
1063    }
1064
1065    public function getFunctionName(): string {
1066        return $this->name->getLast();
1067    }
1068
1069    public function getDeclaration(): string {
1070        return "ZEND_FUNCTION({$this->getDeclarationName()});\n";
1071    }
1072
1073    public function getArgInfoName(): string {
1074        $underscoreName = implode('_', $this->name->getParts());
1075        return "arginfo_$underscoreName";
1076    }
1077
1078    public function getFramelessFunctionInfosName(): string {
1079        $underscoreName = implode('_', $this->name->getParts());
1080        return "frameless_function_infos_$underscoreName";
1081    }
1082
1083    public function getMethodSynopsisFilename(): string {
1084        return 'functions/' . implode('/', str_replace('_', '-', $this->name->getParts()));
1085    }
1086
1087    public function getNameForAttributes(): string {
1088        return strtolower($this->name->toString());
1089    }
1090
1091    public function __toString(): string {
1092        return $this->name->toString();
1093    }
1094
1095    public function isMethod(): bool {
1096        return false;
1097    }
1098
1099    public function isConstructor(): bool {
1100        return false;
1101    }
1102
1103    public function isDestructor(): bool {
1104        return false;
1105    }
1106}
1107
1108class MethodName implements FunctionOrMethodName {
1109    public Name $className;
1110    public string $methodName;
1111
1112    public function __construct(Name $className, string $methodName) {
1113        $this->className = $className;
1114        $this->methodName = $methodName;
1115    }
1116
1117    public function getDeclarationClassName(): string {
1118        return implode('_', $this->className->getParts());
1119    }
1120
1121    public function getDeclaration(): string {
1122        return "ZEND_METHOD({$this->getDeclarationClassName()}, $this->methodName);\n";
1123    }
1124
1125    public function getArgInfoName(): string {
1126        return "arginfo_class_{$this->getDeclarationClassName()}_{$this->methodName}";
1127    }
1128
1129    public function getMethodSynopsisFilename(): string
1130    {
1131        $parts = [...$this->className->getParts(), ltrim($this->methodName, '_')];
1132        /* File paths are in lowercase */
1133        return strtolower(implode('/', $parts));
1134    }
1135
1136    public function getNameForAttributes(): string {
1137        return strtolower($this->methodName);
1138    }
1139
1140    public function __toString(): string {
1141        return "$this->className::$this->methodName";
1142    }
1143
1144    public function isMethod(): bool {
1145        return true;
1146    }
1147
1148    public function isConstructor(): bool {
1149        return $this->methodName === "__construct";
1150    }
1151
1152    public function isDestructor(): bool {
1153        return $this->methodName === "__destruct";
1154    }
1155}
1156
1157class ReturnInfo {
1158    const REFCOUNT_0 = "0";
1159    const REFCOUNT_1 = "1";
1160    const REFCOUNT_N = "N";
1161
1162    const REFCOUNTS_NONSCALAR = [
1163        self::REFCOUNT_1,
1164        self::REFCOUNT_N,
1165    ];
1166
1167    public bool $byRef;
1168    public ?Type $type;
1169    public ?Type $phpDocType;
1170    public bool $tentativeReturnType;
1171    public string $refcount;
1172
1173    public function __construct(bool $byRef, ?Type $type, ?Type $phpDocType, bool $tentativeReturnType, ?string $refcount) {
1174        $this->byRef = $byRef;
1175        $this->setTypes($type, $phpDocType, $tentativeReturnType);
1176        $this->setRefcount($refcount);
1177    }
1178
1179    public function equalsApartFromPhpDocAndRefcount(ReturnInfo $other): bool {
1180        return $this->byRef === $other->byRef
1181            && Type::equals($this->type, $other->type)
1182            && $this->tentativeReturnType === $other->tentativeReturnType;
1183    }
1184
1185    public function getMethodSynopsisType(): ?Type {
1186        return $this->type ?? $this->phpDocType;
1187    }
1188
1189    private function setTypes(?Type $type, ?Type $phpDocType, bool $tentativeReturnType): void
1190    {
1191        $this->type = $type;
1192        $this->phpDocType = $phpDocType;
1193        $this->tentativeReturnType = $tentativeReturnType;
1194    }
1195
1196    private function setRefcount(?string $refcount): void
1197    {
1198        $type = $this->phpDocType ?? $this->type;
1199        $isScalarType = $type !== null && $type->isScalar();
1200
1201        if ($refcount === null) {
1202            $this->refcount = $isScalarType ? self::REFCOUNT_0 : self::REFCOUNT_N;
1203            return;
1204        }
1205
1206        if ($isScalarType) {
1207            throw new Exception(
1208                "@refcount on functions returning scalar values is redundant and not permitted"
1209            );
1210        }
1211
1212        if (!in_array($refcount, ReturnInfo::REFCOUNTS_NONSCALAR, true)) {
1213            throw new Exception("@refcount must have one of the following values: \"1\", \"N\", $refcount given");
1214        }
1215
1216        $this->refcount = $refcount;
1217    }
1218}
1219
1220class FuncInfo {
1221    public FunctionOrMethodName $name;
1222    public int $classFlags;
1223    public int $flags;
1224    public ?string $aliasType;
1225    public ?FunctionOrMethodName $alias;
1226    public bool $isDeprecated;
1227    public bool $supportsCompileTimeEval;
1228    public bool $verify;
1229    /** @var ArgInfo[] */
1230    public array $args;
1231    public ReturnInfo $return;
1232    public int $numRequiredArgs;
1233    public ?string $cond;
1234    public bool $isUndocumentable;
1235    public ?int $minimumPhpVersionIdCompatibility;
1236    /** @var AttributeInfo[] */
1237    public array $attributes;
1238    /** @var FramelessFunctionInfo[] */
1239    public array $framelessFunctionInfos;
1240    public ?ExposedDocComment $exposedDocComment;
1241
1242    /**
1243     * @param ArgInfo[] $args
1244     * @param AttributeInfo[] $attribute
1245     * @param FramelessFunctionInfo[] $framelessFunctionInfos
1246     */
1247    public function __construct(
1248        FunctionOrMethodName $name,
1249        int $classFlags,
1250        int $flags,
1251        ?string $aliasType,
1252        ?FunctionOrMethodName $alias,
1253        bool $isDeprecated,
1254        bool $supportsCompileTimeEval,
1255        bool $verify,
1256        array $args,
1257        ReturnInfo $return,
1258        int $numRequiredArgs,
1259        ?string $cond,
1260        bool $isUndocumentable,
1261        ?int $minimumPhpVersionIdCompatibility,
1262        array $attributes,
1263        array $framelessFunctionInfos,
1264        ?ExposedDocComment $exposedDocComment
1265    ) {
1266        $this->name = $name;
1267        $this->classFlags = $classFlags;
1268        $this->flags = $flags;
1269        $this->aliasType = $aliasType;
1270        $this->alias = $alias;
1271        $this->isDeprecated = $isDeprecated;
1272        $this->supportsCompileTimeEval = $supportsCompileTimeEval;
1273        $this->verify = $verify;
1274        $this->args = $args;
1275        $this->return = $return;
1276        $this->numRequiredArgs = $numRequiredArgs;
1277        $this->cond = $cond;
1278        $this->isUndocumentable = $isUndocumentable;
1279        $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility;
1280        $this->attributes = $attributes;
1281        $this->framelessFunctionInfos = $framelessFunctionInfos;
1282        $this->exposedDocComment = $exposedDocComment;
1283        if ($return->tentativeReturnType && $this->isFinalMethod()) {
1284            throw new Exception("Tentative return inapplicable for final method");
1285        }
1286    }
1287
1288    public function isMethod(): bool
1289    {
1290        return $this->name->isMethod();
1291    }
1292
1293    public function isFinalMethod(): bool
1294    {
1295        return ($this->flags & Modifiers::FINAL) || ($this->classFlags & Modifiers::FINAL);
1296    }
1297
1298    public function isInstanceMethod(): bool
1299    {
1300        return !($this->flags & Modifiers::STATIC) && $this->isMethod() && !$this->name->isConstructor();
1301    }
1302
1303    /** @return string[] */
1304    public function getModifierNames(): array
1305    {
1306        if (!$this->isMethod()) {
1307            return [];
1308        }
1309
1310        $result = [];
1311
1312        if ($this->flags & Modifiers::FINAL) {
1313            $result[] = "final";
1314        } elseif ($this->flags & Modifiers::ABSTRACT && $this->classFlags & ~Modifiers::ABSTRACT) {
1315            $result[] = "abstract";
1316        }
1317
1318        if ($this->flags & Modifiers::PROTECTED) {
1319            $result[] = "protected";
1320        } elseif ($this->flags & Modifiers::PRIVATE) {
1321            $result[] = "private";
1322        } else {
1323            $result[] = "public";
1324        }
1325
1326        if ($this->flags & Modifiers::STATIC) {
1327            $result[] = "static";
1328        }
1329
1330        return $result;
1331    }
1332
1333    public function hasParamWithUnknownDefaultValue(): bool
1334    {
1335        foreach ($this->args as $arg) {
1336            if ($arg->defaultValue && !$arg->hasProperDefaultValue()) {
1337                return true;
1338            }
1339        }
1340
1341        return false;
1342    }
1343
1344    public function equalsApartFromNameAndRefcount(FuncInfo $other): bool {
1345        if (count($this->args) !== count($other->args)) {
1346            return false;
1347        }
1348
1349        for ($i = 0; $i < count($this->args); $i++) {
1350            if (!$this->args[$i]->equals($other->args[$i])) {
1351                return false;
1352            }
1353        }
1354
1355        return $this->return->equalsApartFromPhpDocAndRefcount($other->return)
1356            && $this->numRequiredArgs === $other->numRequiredArgs
1357            && $this->cond === $other->cond;
1358    }
1359
1360    public function getArgInfoName(): string {
1361        return $this->name->getArgInfoName();
1362    }
1363
1364    public function getDeclarationKey(): string
1365    {
1366        $name = $this->alias ?? $this->name;
1367
1368        return "$name|$this->cond";
1369    }
1370
1371    public function getDeclaration(): ?string
1372    {
1373        if ($this->flags & Modifiers::ABSTRACT) {
1374            return null;
1375        }
1376
1377        $name = $this->alias ?? $this->name;
1378
1379        return $name->getDeclaration();
1380    }
1381
1382    public function getFramelessDeclaration(FuncInfo $funcInfo): ?string {
1383        if (empty($this->framelessFunctionInfos)) {
1384            return null;
1385        }
1386
1387        $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID;
1388
1389        $code = '';
1390
1391        if (!$php84MinimumCompatibility) {
1392            $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n";
1393        }
1394
1395        foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) {
1396            $code .= "ZEND_FRAMELESS_FUNCTION({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity});\n";
1397        }
1398
1399        $code .= 'static const zend_frameless_function_info ' . $this->getFramelessFunctionInfosName() . "[] = {\n";
1400        foreach ($this->framelessFunctionInfos as $framelessFunctionInfo) {
1401            $code .= "\t{ ZEND_FRAMELESS_FUNCTION_NAME({$this->name->getFunctionName()}, {$framelessFunctionInfo->arity}), {$framelessFunctionInfo->arity} },\n";
1402        }
1403        $code .= "\t{ 0 },\n";
1404        $code .= "};\n";
1405
1406        if (!$php84MinimumCompatibility) {
1407            $code .= "#endif\n";
1408        }
1409
1410        return $code;
1411    }
1412
1413    public function getFramelessFunctionInfosName(): string {
1414        return $this->name->getFramelessFunctionInfosName();
1415    }
1416
1417    public function getFunctionEntry(): string {
1418        $code = "";
1419
1420        $php84MinimumCompatibility = $this->minimumPhpVersionIdCompatibility === null || $this->minimumPhpVersionIdCompatibility >= PHP_84_VERSION_ID;
1421        $isVanillaEntry = $this->alias === null && !$this->supportsCompileTimeEval && $this->exposedDocComment === null && empty($this->framelessFunctionInfos);
1422        $argInfoName = $this->getArgInfoName();
1423        $flagsByPhpVersions = $this->getArginfoFlagsByPhpVersions();
1424        $functionEntryCode = null;
1425
1426        if (!empty($this->framelessFunctionInfos)) {
1427            if ($this->isMethod()) {
1428                throw new Exception('Frameless methods are not supported yet');
1429            }
1430            if ($this->name->getNamespace()) {
1431                throw new Exception('Namespaced direct calls to frameless functions are not supported yet');
1432            }
1433            if ($this->alias) {
1434                throw new Exception('Aliased direct calls to frameless functions are not supported yet');
1435            }
1436        }
1437
1438        if ($this->isMethod()) {
1439            $zendName = '"' . $this->name->methodName . '"';
1440            if ($this->alias) {
1441                if ($this->alias instanceof MethodName) {
1442                    $name = "zim_" . $this->alias->getDeclarationClassName() . "_" . $this->alias->methodName;
1443                } else if ($this->alias instanceof FunctionName) {
1444                    $name = "zif_" . $this->alias->getNonNamespacedName();
1445                } else {
1446                    throw new Error("Cannot happen");
1447                }
1448            } else {
1449                if ($this->flags & Modifiers::ABSTRACT) {
1450                    $name = "NULL";
1451                } else {
1452                    $name = "zim_" . $this->name->getDeclarationClassName() . "_" . $this->name->methodName;
1453
1454                    if ($isVanillaEntry) {
1455                        $functionEntryCode = "\tZEND_ME(" . $this->name->getDeclarationClassName() . ", " . $this->name->methodName . ", $argInfoName, " . implode("|", reset($flagsByPhpVersions)) . ")";
1456                    }
1457                }
1458            }
1459        } else if ($this->name instanceof FunctionName) {
1460            $functionName = $this->name->getFunctionName();
1461            $declarationName = $this->alias ? $this->alias->getNonNamespacedName() : $this->name->getDeclarationName();
1462
1463            if ($this->name->getNamespace()) {
1464                $namespace = addslashes($this->name->getNamespace());
1465                $zendName = "ZEND_NS_NAME(\"$namespace\", \"$functionName\")";
1466                $name = "zif_$declarationName";
1467            } else {
1468                $zendName = '"' . $functionName . '"';
1469                $name = "zif_$declarationName";
1470
1471                if ($isVanillaEntry && reset($flagsByPhpVersions) === ["0"]) {
1472                    $functionEntryCode = "\tZEND_FE($declarationName, $argInfoName)";
1473                }
1474            }
1475        } else {
1476            throw new Error("Cannot happen");
1477        }
1478
1479        if ($functionEntryCode !== null) {
1480            $code .= "$functionEntryCode\n";
1481        } else {
1482            if (!$php84MinimumCompatibility) {
1483                $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n";
1484            }
1485
1486            $php84AndAboveFlags = array_slice($flagsByPhpVersions, 5, null, true);
1487            $docComment = $this->exposedDocComment ? '"' . $this->exposedDocComment->escape() . '"' : "NULL";
1488            $framelessFuncInfosName = !empty($this->framelessFunctionInfos) ? $this->getFramelessFunctionInfosName() : "NULL";
1489
1490            $template = "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s, $framelessFuncInfosName, $docComment)\n";
1491            $flagsCode = generateVersionDependentFlagCode(
1492                $template,
1493                $php84AndAboveFlags,
1494                PHP_84_VERSION_ID
1495            );
1496            $code .= implode("", $flagsCode);
1497
1498            if (!$php84MinimumCompatibility) {
1499                $code .= "#else\n";
1500            }
1501
1502            if (!$php84MinimumCompatibility) {
1503                $flags = array_slice($flagsByPhpVersions, 0, 4, true);
1504                $template = "\tZEND_RAW_FENTRY($zendName, $name, $argInfoName, %s)\n";
1505                $flagsCode = generateVersionDependentFlagCode(
1506                    $template,
1507                    $flags,
1508                    $this->minimumPhpVersionIdCompatibility
1509                );
1510                $code .= implode("", $flagsCode);
1511            }
1512
1513            if (!$php84MinimumCompatibility) {
1514                $code .= "#endif\n";
1515            }
1516        }
1517
1518        return $code;
1519    }
1520
1521    public function getOptimizerInfo(): ?string {
1522        if ($this->isMethod()) {
1523            return null;
1524        }
1525
1526        if ($this->alias !== null) {
1527            return null;
1528        }
1529
1530        if ($this->return->refcount !== ReturnInfo::REFCOUNT_1 && $this->return->phpDocType === null) {
1531            return null;
1532        }
1533
1534        $type = $this->return->phpDocType ?? $this->return->type;
1535        if ($type === null) {
1536            return null;
1537        }
1538
1539        return "\tF" . $this->return->refcount . '("' . addslashes($this->name->__toString()) . '", ' . $type->toOptimizerTypeMask() . "),\n";
1540    }
1541
1542    public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void {
1543        $this->attributes = [];
1544        $this->return->type = null;
1545        $this->framelessFunctionInfos = [];
1546        $this->exposedDocComment = null;
1547        $this->supportsCompileTimeEval = false;
1548        foreach ($this->args as $arg) {
1549            $arg->type = null;
1550            $arg->defaultValue = null;
1551            $arg->attributes = [];
1552        }
1553        $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility;
1554    }
1555
1556    /** @return array<int, string[]> */
1557    private function getArginfoFlagsByPhpVersions(): array
1558    {
1559        $flags = [];
1560
1561        if ($this->isMethod()) {
1562            if ($this->flags & Modifiers::PROTECTED) {
1563                $flags[] = "ZEND_ACC_PROTECTED";
1564            } elseif ($this->flags & Modifiers::PRIVATE) {
1565                $flags[] = "ZEND_ACC_PRIVATE";
1566            } else {
1567                $flags[] = "ZEND_ACC_PUBLIC";
1568            }
1569
1570            if ($this->flags & Modifiers::STATIC) {
1571                $flags[] = "ZEND_ACC_STATIC";
1572            }
1573
1574            if ($this->flags & Modifiers::FINAL) {
1575                $flags[] = "ZEND_ACC_FINAL";
1576            }
1577
1578            if ($this->flags & Modifiers::ABSTRACT) {
1579                $flags[] = "ZEND_ACC_ABSTRACT";
1580            }
1581        }
1582
1583        if ($this->isDeprecated) {
1584            $flags[] = "ZEND_ACC_DEPRECATED";
1585        }
1586
1587        foreach ($this->attributes as $attr) {
1588            if ($attr->class === "Deprecated") {
1589                $flags[] = "ZEND_ACC_DEPRECATED";
1590                break;
1591            }
1592        }
1593
1594        $php82AndAboveFlags = $flags;
1595        if ($this->isMethod() === false && $this->supportsCompileTimeEval) {
1596            $php82AndAboveFlags[] = "ZEND_ACC_COMPILE_TIME_EVAL";
1597        }
1598
1599        if (empty($flags)) {
1600            $flags[] = "0";
1601        }
1602        if (empty($php82AndAboveFlags)) {
1603            $php82AndAboveFlags[] = "0";
1604        }
1605
1606        return [
1607            PHP_70_VERSION_ID => $flags,
1608            PHP_80_VERSION_ID => $flags,
1609            PHP_81_VERSION_ID => $flags,
1610            PHP_82_VERSION_ID => $php82AndAboveFlags,
1611            PHP_83_VERSION_ID => $php82AndAboveFlags,
1612            PHP_84_VERSION_ID => $php82AndAboveFlags,
1613        ];
1614    }
1615
1616    private function generateRefSect1(DOMDocument $doc, string $role): DOMElement {
1617        $refSec = $doc->createElement('refsect1');
1618        $refSec->setAttribute('role', $role);
1619        $refSec->append(
1620            "\n  ",
1621            $doc->createEntityReference('reftitle.' . $role),
1622            "\n  "
1623        );
1624        return $refSec;
1625    }
1626
1627    /**
1628     * @param array<string, FuncInfo> $funcMap
1629     * @param array<string, FuncInfo> $aliasMap
1630     * @throws Exception
1631     */
1632    public function getMethodSynopsisDocument(array $funcMap, array $aliasMap): ?string {
1633        $REFSEC1_SEPERATOR = "\n\n ";
1634
1635        $doc = new DOMDocument("1.0", "utf-8");
1636        $doc->formatOutput = true;
1637
1638        $refentry = $doc->createElement('refentry');
1639        $doc->appendChild($refentry);
1640
1641        if ($this->isMethod()) {
1642            assert($this->name instanceof MethodName);
1643            /* Namespaces are seperated by '-', '_' must be converted to '-' too.
1644             * Trim away the __ for magic methods */
1645            $id = strtolower(
1646                str_replace('\\', '-', $this->name->className->__toString())
1647                . '.'
1648                . str_replace('_', '-', ltrim($this->name->methodName, '_'))
1649            );
1650        } else {
1651            $id = 'function.' . strtolower(str_replace('_', '-', $this->name->__toString()));
1652        }
1653        $refentry->setAttribute("xml:id", $id);
1654        /* We create an attribute for xmlns, as libxml otherwise force it to be the first one */
1655        //$refentry->setAttribute("xmlns", "http://docbook.org/ns/docbook");
1656        $namespace = $doc->createAttribute('xmlns');
1657        $namespace->value = "http://docbook.org/ns/docbook";
1658        $refentry->setAttributeNode($namespace);
1659        $refentry->setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
1660        $refentry->appendChild(new DOMText("\n "));
1661
1662        /* Creation of <refnamediv> */
1663        $refnamediv = $doc->createElement('refnamediv');
1664        $refnamediv->appendChild(new DOMText("\n  "));
1665        $refname = $doc->createElement('refname', $this->name->__toString());
1666        $refnamediv->appendChild($refname);
1667        $refnamediv->appendChild(new DOMText("\n  "));
1668        $refpurpose = $doc->createElement('refpurpose', 'Description');
1669        $refnamediv->appendChild($refpurpose);
1670
1671        $refnamediv->appendChild(new DOMText("\n "));
1672        $refentry->append($refnamediv, $REFSEC1_SEPERATOR);
1673
1674        /* Creation of <refsect1 role="description"> */
1675        $descriptionRefSec = $this->generateRefSect1($doc, 'description');
1676
1677        $methodSynopsis = $this->getMethodSynopsisElement($funcMap, $aliasMap, $doc);
1678        if (!$methodSynopsis) {
1679            return null;
1680        }
1681        $descriptionRefSec->appendChild($methodSynopsis);
1682        $descriptionRefSec->appendChild(new DOMText("\n  "));
1683        $undocumentedEntity = $doc->createEntityReference('warn.undocumented.func');
1684        $descriptionRefSec->appendChild($undocumentedEntity);
1685        $descriptionRefSec->appendChild(new DOMText("\n  "));
1686        $returnDescriptionPara = $doc->createElement('simpara');
1687        $returnDescriptionPara->appendChild(new DOMText("\n   Description.\n  "));
1688        $descriptionRefSec->appendChild($returnDescriptionPara);
1689
1690        $descriptionRefSec->appendChild(new DOMText("\n "));
1691        $refentry->append($descriptionRefSec, $REFSEC1_SEPERATOR);
1692
1693        /* Creation of <refsect1 role="parameters"> */
1694        $parametersRefSec = $this->getParameterSection($doc);
1695        $refentry->append($parametersRefSec, $REFSEC1_SEPERATOR);
1696
1697        /* Creation of <refsect1 role="returnvalues"> */
1698        if (!$this->name->isConstructor() && !$this->name->isDestructor()) {
1699            $returnRefSec = $this->getReturnValueSection($doc);
1700            $refentry->append($returnRefSec, $REFSEC1_SEPERATOR);
1701        }
1702
1703        /* Creation of <refsect1 role="errors"> */
1704        $errorsRefSec = $this->generateRefSect1($doc, 'errors');
1705        $errorsDescriptionParaConstantTag = $doc->createElement('constant');
1706        $errorsDescriptionParaConstantTag->append('E_*');
1707        $errorsDescriptionParaExceptionTag = $doc->createElement('exceptionname');
1708        $errorsDescriptionParaExceptionTag->append('Exception');
1709        $errorsDescriptionPara = $doc->createElement('simpara');
1710        $errorsDescriptionPara->append(
1711            "\n   When does this function issue ",
1712            $errorsDescriptionParaConstantTag,
1713            " level errors,\n   and/or throw ",
1714            $errorsDescriptionParaExceptionTag,
1715            "s.\n  "
1716        );
1717        $errorsRefSec->appendChild($errorsDescriptionPara);
1718        $errorsRefSec->appendChild(new DOMText("\n "));
1719
1720        $refentry->append($errorsRefSec, $REFSEC1_SEPERATOR);
1721
1722        /* Creation of <refsect1 role="changelog"> */
1723        $changelogRefSec = $this->getChangelogSection($doc);
1724        $refentry->append($changelogRefSec, $REFSEC1_SEPERATOR);
1725
1726        $exampleRefSec = $this->getExampleSection($doc, $id);
1727        $refentry->append($exampleRefSec, $REFSEC1_SEPERATOR);
1728
1729        /* Creation of <refsect1 role="notes"> */
1730        $notesRefSec = $this->generateRefSect1($doc, 'notes');
1731
1732        $noteTagSimara = $doc->createElement('simpara');
1733        $noteTagSimara->append(
1734            "\n    Any notes that don't fit anywhere else should go here.\n   "
1735        );
1736        $noteTag = $doc->createElement('note');
1737        $noteTag->append("\n   ", $noteTagSimara, "\n  ");
1738        $notesRefSec->append($noteTag, "\n ");
1739
1740        $refentry->append($notesRefSec, $REFSEC1_SEPERATOR);
1741
1742        /* Creation of <refsect1 role="seealso"> */
1743        $seeAlsoRefSec = $this->generateRefSect1($doc, 'seealso');
1744
1745        $seeAlsoMemberClassMethod = $doc->createElement('member');
1746        $seeAlsoMemberClassMethodTag = $doc->createElement('methodname');
1747        $seeAlsoMemberClassMethodTag->appendChild(new DOMText("ClassName::otherMethodName"));
1748        $seeAlsoMemberClassMethod->appendChild($seeAlsoMemberClassMethodTag);
1749
1750        $seeAlsoMemberFunction = $doc->createElement('member');
1751        $seeAlsoMemberFunctionTag = $doc->createElement('function');
1752        $seeAlsoMemberFunctionTag->appendChild(new DOMText("some_function"));
1753        $seeAlsoMemberFunction->appendChild($seeAlsoMemberFunctionTag);
1754
1755        $seeAlsoMemberLink = $doc->createElement('member');
1756        $seeAlsoMemberLinkTag = $doc->createElement('link');
1757        $seeAlsoMemberLinkTag->setAttribute('linkend', 'some.id.chunk.to.link');
1758        $seeAlsoMemberLinkTag->appendChild(new DOMText('something appendix'));
1759        $seeAlsoMemberLink->appendChild($seeAlsoMemberLinkTag);
1760
1761        $seeAlsoList = $doc->createElement('simplelist');
1762        $seeAlsoList->append(
1763            "\n   ",
1764            $seeAlsoMemberClassMethod,
1765            "\n   ",
1766            $seeAlsoMemberFunction,
1767            "\n   ",
1768            $seeAlsoMemberLink,
1769            "\n  "
1770        );
1771
1772        $seeAlsoRefSec->appendChild($seeAlsoList);
1773        $seeAlsoRefSec->appendChild(new DOMText("\n "));
1774
1775        $refentry->appendChild($seeAlsoRefSec);
1776
1777        $refentry->appendChild(new DOMText("\n\n"));
1778
1779        $doc->appendChild(new DOMComment(
1780            <<<ENDCOMMENT
1781 Keep this comment at the end of the file
1782Local variables:
1783mode: sgml
1784sgml-omittag:t
1785sgml-shorttag:t
1786sgml-minimize-attributes:nil
1787sgml-always-quote-attributes:t
1788sgml-indent-step:1
1789sgml-indent-data:t
1790indent-tabs-mode:nil
1791sgml-parent-document:nil
1792sgml-default-dtd-file:"~/.phpdoc/manual.ced"
1793sgml-exposed-tags:nil
1794sgml-local-catalogs:nil
1795sgml-local-ecat-files:nil
1796End:
1797vim600: syn=xml fen fdm=syntax fdl=2 si
1798vim: et tw=78 syn=sgml
1799vi: ts=1 sw=1
1800
1801ENDCOMMENT
1802        ));
1803        return $doc->saveXML();
1804    }
1805
1806    private function getParameterSection(DOMDocument $doc): DOMElement {
1807        $parametersRefSec = $this->generateRefSect1($doc, 'parameters');
1808        if (empty($this->args)) {
1809            $noParamEntity = $doc->createEntityReference('no.function.parameters');
1810            $parametersRefSec->appendChild($noParamEntity);
1811            return $parametersRefSec;
1812        } else {
1813            $parametersContainer = $doc->createDocumentFragment();
1814
1815            $parametersContainer->appendChild(new DOMText("\n  "));
1816            $parametersList = $doc->createElement('variablelist');
1817            $parametersContainer->appendChild($parametersList);
1818
1819            /*
1820            <varlistentry>
1821             <term><parameter>name</parameter></term>
1822             <listitem>
1823              <simpara>
1824               Description.
1825              </simpara>
1826             </listitem>
1827            </varlistentry>
1828            */
1829            foreach ($this->args as $arg) {
1830                $parameter = $doc->createElement('parameter', $arg->name);
1831                $parameterTerm = $doc->createElement('term');
1832                $parameterTerm->appendChild($parameter);
1833
1834                $listItemPara = $doc->createElement('simpara');
1835                $listItemPara->append(
1836                    "\n      ",
1837                    "Description.",
1838                    "\n     ",
1839                );
1840
1841                $parameterEntryListItem = $doc->createElement('listitem');
1842                $parameterEntryListItem->append(
1843                    "\n     ",
1844                    $listItemPara,
1845                    "\n    ",
1846                );
1847
1848                $parameterEntry = $doc->createElement('varlistentry');
1849                $parameterEntry->append(
1850                    "\n    ",
1851                    $parameterTerm,
1852                    "\n    ",
1853                    $parameterEntryListItem,
1854                    "\n   ",
1855                );
1856
1857                $parametersList->appendChild(new DOMText("\n   "));
1858                $parametersList->appendChild($parameterEntry);
1859            }
1860            $parametersList->appendChild(new DOMText("\n  "));
1861        }
1862        $parametersContainer->appendChild(new DOMText("\n "));
1863        $parametersRefSec->appendChild($parametersContainer);
1864        $parametersRefSec->appendChild(new DOMText("\n "));
1865        return $parametersRefSec;
1866    }
1867
1868    private function getReturnValueSection(DOMDocument $doc): DOMElement {
1869        $returnRefSec = $this->generateRefSect1($doc, 'returnvalues');
1870
1871        $returnDescriptionPara = $doc->createElement('simpara');
1872        $returnDescriptionPara->appendChild(new DOMText("\n   "));
1873
1874        $returnType = $this->return->getMethodSynopsisType();
1875        if ($returnType === null) {
1876            $returnDescriptionPara->appendChild(new DOMText("Description."));
1877        } else if (count($returnType->types) === 1) {
1878            $type = $returnType->types[0];
1879            $name = $type->name;
1880
1881            switch ($name) {
1882                case 'void':
1883                    $descriptionNode = $doc->createEntityReference('return.void');
1884                    break;
1885                case 'true':
1886                    $descriptionNode = $doc->createEntityReference('return.true.always');
1887                    break;
1888                case 'bool':
1889                    $descriptionNode = $doc->createEntityReference('return.success');
1890                    break;
1891                default:
1892                    $descriptionNode = new DOMText("Description.");
1893                    break;
1894            }
1895            $returnDescriptionPara->appendChild($descriptionNode);
1896        } else {
1897            $returnDescriptionPara->appendChild(new DOMText("Description."));
1898        }
1899        $returnDescriptionPara->appendChild(new DOMText("\n  "));
1900        $returnRefSec->appendChild($returnDescriptionPara);
1901        $returnRefSec->appendChild(new DOMText("\n "));
1902        return $returnRefSec;
1903    }
1904
1905    /**
1906     * @param array<DOMNode> $headers [count($headers) === $columns]
1907     * @param array<array<DOMNode>> $rows [count($rows[$i]) === $columns]
1908     */
1909    private function generateDocbookInformalTable(
1910        DOMDocument $doc,
1911        int $indent,
1912        int $columns,
1913        array $headers,
1914        array $rows
1915    ): DOMElement {
1916        $strIndent = str_repeat(' ', $indent);
1917
1918        $headerRow = $doc->createElement('row');
1919        foreach ($headers as $header) {
1920            $headerEntry = $doc->createElement('entry');
1921            $headerEntry->appendChild($header);
1922
1923            $headerRow->append("\n$strIndent    ", $headerEntry);
1924        }
1925        $headerRow->append("\n$strIndent   ");
1926
1927        $thead = $doc->createElement('thead');
1928        $thead->append(
1929            "\n$strIndent   ",
1930            $headerRow,
1931            "\n$strIndent  ",
1932        );
1933
1934        $tbody = $doc->createElement('tbody');
1935        foreach ($rows as $row) {
1936            $bodyRow = $doc->createElement('row');
1937            foreach ($row as $cell) {
1938                $entry = $doc->createElement('entry');
1939                $entry->appendChild($cell);
1940
1941                $bodyRow->appendChild(new DOMText("\n$strIndent    "));
1942                $bodyRow->appendChild($entry);
1943            }
1944            $bodyRow->appendChild(new DOMText("\n$strIndent   "));
1945
1946            $tbody->append(
1947                "\n$strIndent   ",
1948                $bodyRow,
1949                "\n$strIndent  ",
1950            );
1951        }
1952
1953        $tgroup = $doc->createElement('tgroup');
1954        $tgroup->setAttribute('cols', (string) $columns);
1955        $tgroup->append(
1956            "\n$strIndent  ",
1957            $thead,
1958            "\n$strIndent  ",
1959            $tbody,
1960            "\n$strIndent ",
1961        );
1962
1963        $table = $doc->createElement('informaltable');
1964        $table->append(
1965            "\n$strIndent ",
1966            $tgroup,
1967            "\n$strIndent",
1968        );
1969
1970        return $table;
1971    }
1972
1973    private function getChangelogSection(DOMDocument $doc): DOMElement {
1974        $refSec = $this->generateRefSect1($doc, 'changelog');
1975        $headers = [
1976            $doc->createEntityReference('Version'),
1977            $doc->createEntityReference('Description'),
1978        ];
1979        $rows = [[
1980            new DOMText('8.X.0'),
1981            new DOMText("\n       Description\n      "),
1982        ]];
1983        $table = $this->generateDocbookInformalTable(
1984            $doc,
1985            /* indent: */ 2,
1986            /* columns: */ 2,
1987            /* headers: */ $headers,
1988            /* rows: */ $rows
1989        );
1990        $refSec->appendChild($table);
1991
1992        $refSec->appendChild(new DOMText("\n "));
1993        return $refSec;
1994    }
1995
1996    private function getExampleSection(DOMDocument $doc, string $id): DOMElement {
1997        $refSec = $this->generateRefSect1($doc, 'examples');
1998
1999        $example = $doc->createElement('example');
2000        $fnName = $this->name->__toString();
2001        $example->setAttribute('xml:id', $id . '.example.basic');
2002
2003        $title = $doc->createElement('title');
2004        $fn = $doc->createElement($this->isMethod() ? 'methodname' : 'function');
2005        $fn->append($fnName);
2006        $title->append($fn, ' example');
2007
2008        $example->append("\n   ", $title);
2009
2010        $para = $doc->createElement('simpara');
2011        $para->append("\n    ", "Description.", "\n   ");
2012        $example->append("\n   ", $para);
2013
2014        $prog = $doc->createElement('programlisting');
2015        $prog->setAttribute('role', 'php');
2016        $code = new DOMCdataSection(
2017            <<<CODE_EXAMPLE
2018
2019<?php
2020echo "Code example";
2021?>
2022
2023CODE_EXAMPLE
2024        );
2025        $prog->append("\n");
2026        $prog->appendChild($code);
2027        $prog->append("\n   ");
2028
2029        $example->append("\n   ", $prog);
2030        $example->append("\n   ", $doc->createEntityReference('example.outputs'));
2031
2032        $output = new DOMCdataSection(
2033            <<<OUPUT_EXAMPLE
2034
2035Code example
2036
2037OUPUT_EXAMPLE
2038        );
2039        $screen = $doc->createElement('screen');
2040        $screen->append("\n");
2041        $screen->appendChild($output);
2042        $screen->append("\n   ");
2043
2044        $example->append(
2045            "\n   ",
2046            $screen,
2047            "\n  ",
2048        );
2049
2050        $refSec->append(
2051            $example,
2052            "\n ",
2053        );
2054        return $refSec;
2055    }
2056
2057    /**
2058     * @param array<string, FuncInfo> $funcMap
2059     * @param array<string, FuncInfo> $aliasMap
2060     * @throws Exception
2061     */
2062    public function getMethodSynopsisElement(array $funcMap, array $aliasMap, DOMDocument $doc): ?DOMElement {
2063        if ($this->hasParamWithUnknownDefaultValue()) {
2064            return null;
2065        }
2066
2067        if ($this->name->isConstructor()) {
2068            $synopsisType = "constructorsynopsis";
2069        } elseif ($this->name->isDestructor()) {
2070            $synopsisType = "destructorsynopsis";
2071        } else {
2072            $synopsisType = "methodsynopsis";
2073        }
2074
2075        $methodSynopsis = $doc->createElement($synopsisType);
2076
2077        if ($this->isMethod()) {
2078            assert($this->name instanceof MethodName);
2079            $role = $doc->createAttribute("role");
2080            $role->value = addslashes($this->name->className->__toString());
2081            $methodSynopsis->appendChild($role);
2082        }
2083
2084        $methodSynopsis->appendChild(new DOMText("\n   "));
2085
2086        foreach ($this->attributes as $attribute) {
2087            $modifier = $doc->createElement("modifier", "#[\\" . $attribute->class . "]");
2088            $modifier->setAttribute("role", "attribute");
2089            $methodSynopsis->appendChild($modifier);
2090            $methodSynopsis->appendChild(new DOMText("\n   "));
2091        }
2092
2093        foreach ($this->getModifierNames() as $modifierString) {
2094            $modifierElement = $doc->createElement('modifier', $modifierString);
2095            $methodSynopsis->appendChild($modifierElement);
2096            $methodSynopsis->appendChild(new DOMText(" "));
2097        }
2098
2099        $returnType = $this->return->getMethodSynopsisType();
2100        if ($returnType) {
2101            $methodSynopsis->appendChild($returnType->getTypeForDoc($doc));
2102        }
2103
2104        $methodname = $doc->createElement('methodname', $this->name->__toString());
2105        $methodSynopsis->appendChild($methodname);
2106
2107        if (empty($this->args)) {
2108            $methodSynopsis->appendChild(new DOMText("\n   "));
2109            $void = $doc->createElement('void');
2110            $methodSynopsis->appendChild($void);
2111        } else {
2112            foreach ($this->args as $arg) {
2113                $methodSynopsis->appendChild(new DOMText("\n   "));
2114                $methodparam = $doc->createElement('methodparam');
2115                if ($arg->defaultValue !== null) {
2116                    $methodparam->setAttribute("choice", "opt");
2117                }
2118                if ($arg->isVariadic) {
2119                    $methodparam->setAttribute("rep", "repeat");
2120                }
2121
2122                $methodSynopsis->appendChild($methodparam);
2123                foreach ($arg->attributes as $attribute) {
2124                    $attribute = $doc->createElement("modifier", "#[\\" . $attribute->class . "]");
2125                    $attribute->setAttribute("role", "attribute");
2126
2127                    $methodparam->appendChild($attribute);
2128                }
2129
2130                $methodparam->appendChild($arg->getMethodSynopsisType()->getTypeForDoc($doc));
2131
2132                $parameter = $doc->createElement('parameter', $arg->name);
2133                if ($arg->sendBy !== ArgInfo::SEND_BY_VAL) {
2134                    $parameter->setAttribute("role", "reference");
2135                }
2136
2137                $methodparam->appendChild($parameter);
2138                $defaultValue = $arg->getDefaultValueAsMethodSynopsisString();
2139                if ($defaultValue !== null) {
2140                    $initializer = $doc->createElement('initializer');
2141                    if (preg_match('/^[a-zA-Z_][a-zA-Z_0-9]*$/', $defaultValue)) {
2142                        $constant = $doc->createElement('constant', $defaultValue);
2143                        $initializer->appendChild($constant);
2144                    } else {
2145                        $initializer->nodeValue = $defaultValue;
2146                    }
2147                    $methodparam->appendChild($initializer);
2148                }
2149            }
2150        }
2151        $methodSynopsis->appendChild(new DOMText("\n  "));
2152
2153        return $methodSynopsis;
2154    }
2155
2156    public function __clone()
2157    {
2158        foreach ($this->args as $key => $argInfo) {
2159            $this->args[$key] = clone $argInfo;
2160        }
2161        $this->return = clone $this->return;
2162        foreach ($this->attributes as $key => $attribute) {
2163            $this->attributes[$key] = clone $attribute;
2164        }
2165        foreach ($this->framelessFunctionInfos as $key => $framelessFunctionInfo) {
2166            $this->framelessFunctionInfos[$key] = clone $framelessFunctionInfo;
2167        }
2168        if ($this->exposedDocComment) {
2169            $this->exposedDocComment = clone $this->exposedDocComment;
2170        }
2171    }
2172}
2173
2174class EvaluatedValue
2175{
2176    /** @var mixed */
2177    public $value;
2178    public SimpleType $type;
2179    public Expr $expr;
2180    public bool $isUnknownConstValue;
2181    /** @var ConstInfo[] */
2182    public array $originatingConsts;
2183
2184    /**
2185     * @param array<string, ConstInfo> $allConstInfos
2186     */
2187    public static function createFromExpression(Expr $expr, ?SimpleType $constType, ?string $cConstName, array $allConstInfos): EvaluatedValue
2188    {
2189        // This visitor replaces the PHP constants by C constants. It allows direct expansion of the compiled constants, e.g. later in the pretty printer.
2190        $visitor = new class($allConstInfos) extends PhpParser\NodeVisitorAbstract
2191        {
2192            /** @var iterable<ConstInfo> */
2193            public array $visitedConstants = [];
2194            /** @var array<string, ConstInfo> */
2195            public array $allConstInfos;
2196
2197            /** @param array<string, ConstInfo> $allConstInfos */
2198            public function __construct(array $allConstInfos)
2199            {
2200                $this->allConstInfos = $allConstInfos;
2201            }
2202
2203            /** @return Node|null */
2204            public function enterNode(Node $expr)
2205            {
2206                if (!$expr instanceof Expr\ConstFetch && !$expr instanceof Expr\ClassConstFetch) {
2207                    return null;
2208                }
2209
2210                if ($expr instanceof Expr\ClassConstFetch) {
2211                    $originatingConstName = new ClassConstName($expr->class, $expr->name->toString());
2212                } else {
2213                    $originatingConstName = new ConstName($expr->name->getAttribute('namespacedName'), $expr->name->toString());
2214                }
2215
2216                if ($originatingConstName->isUnknown()) {
2217                    return null;
2218                }
2219
2220                $const = $this->allConstInfos[$originatingConstName->__toString()] ?? null;
2221                if ($const !== null) {
2222                    $this->visitedConstants[] = $const;
2223                    return $const->getValue($this->allConstInfos)->expr;
2224                }
2225            }
2226        };
2227
2228        $nodeTraverser = new PhpParser\NodeTraverser;
2229        $nodeTraverser->addVisitor($visitor);
2230        $expr = $nodeTraverser->traverse([$expr])[0];
2231
2232        $isUnknownConstValue = false;
2233
2234        $evaluator = new ConstExprEvaluator(
2235            static function (Expr $expr) use ($allConstInfos, &$isUnknownConstValue) {
2236                // $expr is a ConstFetch with a name of a C macro here
2237                if (!$expr instanceof Expr\ConstFetch) {
2238                    throw new Exception($this->getVariableTypeName() . " " . $this->name->__toString() . " has an unsupported value");
2239                }
2240
2241                $constName = $expr->name->__toString();
2242                if (strtolower($constName) === "unknown") {
2243                    $isUnknownConstValue = true;
2244                    return null;
2245                }
2246
2247                foreach ($allConstInfos as $const) {
2248                    if ($constName != $const->cValue) {
2249                        continue;
2250                    }
2251
2252                    $constType = ($const->phpDocType ?? $const->type)->tryToSimpleType();
2253                    if ($constType) {
2254                        if ($constType->isBool()) {
2255                            return true;
2256                        } elseif ($constType->isInt()) {
2257                            return 1;
2258                        } elseif ($constType->isFloat()) {
2259                            return M_PI;
2260                        } elseif ($constType->isString()) {
2261                            return $const->name;
2262                        } elseif ($constType->isArray()) {
2263                            return [];
2264                        }
2265                    }
2266
2267                    return null;
2268                }
2269
2270                throw new Exception("Constant " . $constName . " cannot be found");
2271            }
2272        );
2273
2274        $result = $evaluator->evaluateDirectly($expr);
2275
2276        return new EvaluatedValue(
2277            $result, // note: we are generally not interested in the actual value of $result, unless it's a bare value, without constants
2278            $constType ?? SimpleType::fromValue($result),
2279            $cConstName === null ? $expr : new Expr\ConstFetch(new Node\Name($cConstName)),
2280            $visitor->visitedConstants,
2281            $isUnknownConstValue
2282        );
2283    }
2284
2285    public static function null(): EvaluatedValue
2286    {
2287        return new self(null, SimpleType::null(), new Expr\ConstFetch(new Node\Name('null')), [], false);
2288    }
2289
2290    /**
2291     * @param mixed $value
2292     * @param ConstInfo[] $originatingConsts
2293     */
2294    private function __construct($value, SimpleType $type, Expr $expr, array $originatingConsts, bool $isUnknownConstValue)
2295    {
2296        $this->value = $value;
2297        $this->type = $type;
2298        $this->expr = $expr;
2299        $this->originatingConsts = $originatingConsts;
2300        $this->isUnknownConstValue = $isUnknownConstValue;
2301    }
2302
2303    public function initializeZval(string $zvalName): string
2304    {
2305        $cExpr = $this->getCExpr();
2306
2307        $code = "\tzval $zvalName;\n";
2308
2309        if ($this->type->isNull()) {
2310            $code .= "\tZVAL_NULL(&$zvalName);\n";
2311        } elseif ($this->type->isBool()) {
2312            if ($cExpr == 'true') {
2313                $code .= "\tZVAL_TRUE(&$zvalName);\n";
2314            } elseif ($cExpr == 'false') {
2315                $code .= "\tZVAL_FALSE(&$zvalName);\n";
2316            } else {
2317                $code .= "\tZVAL_BOOL(&$zvalName, $cExpr);\n";
2318            }
2319        } elseif ($this->type->isInt()) {
2320            $code .= "\tZVAL_LONG(&$zvalName, $cExpr);\n";
2321        } elseif ($this->type->isFloat()) {
2322            $code .= "\tZVAL_DOUBLE(&$zvalName, $cExpr);\n";
2323        } elseif ($this->type->isString()) {
2324            if ($cExpr === '""') {
2325                $code .= "\tZVAL_EMPTY_STRING(&$zvalName);\n";
2326            } else {
2327                $code .= "\tzend_string *{$zvalName}_str = zend_string_init($cExpr, strlen($cExpr), 1);\n";
2328                $code .= "\tZVAL_STR(&$zvalName, {$zvalName}_str);\n";
2329            }
2330        } elseif ($this->type->isArray()) {
2331            if ($cExpr == '[]') {
2332                $code .= "\tZVAL_EMPTY_ARRAY(&$zvalName);\n";
2333            } else {
2334                throw new Exception("Unimplemented default value");
2335            }
2336        } else {
2337            throw new Exception("Invalid default value: " . print_r($this->value, true) . ", type: " . print_r($this->type, true));
2338        }
2339
2340        return $code;
2341    }
2342
2343    public function getCExpr(): ?string
2344    {
2345        // $this->expr has all its PHP constants replaced by C constants
2346        $prettyPrinter = new Standard;
2347        $expr = $prettyPrinter->prettyPrintExpr($this->expr);
2348        // PHP single-quote to C double-quote string
2349        if ($this->type->isString()) {
2350            $expr = preg_replace("/(^'|'$)/", '"', $expr);
2351        }
2352        return $expr[0] == '"' ? $expr : preg_replace('(\bnull\b)', 'NULL', str_replace('\\', '', $expr));
2353    }
2354}
2355
2356abstract class VariableLike
2357{
2358    public int $flags;
2359    public ?Type $type;
2360    public ?Type $phpDocType;
2361    public ?string $link;
2362    public ?int $phpVersionIdMinimumCompatibility;
2363    /** @var AttributeInfo[] */
2364    public array $attributes;
2365    public ?ExposedDocComment $exposedDocComment;
2366
2367    /**
2368     * @var AttributeInfo[] $attributes
2369     */
2370    public function __construct(
2371        int $flags,
2372        ?Type $type,
2373        ?Type $phpDocType,
2374        ?string $link,
2375        ?int $phpVersionIdMinimumCompatibility,
2376        array $attributes,
2377        ?ExposedDocComment $exposedDocComment
2378    ) {
2379        $this->flags = $flags;
2380        $this->type = $type;
2381        $this->phpDocType = $phpDocType;
2382        $this->link = $link;
2383        $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
2384        $this->attributes = $attributes;
2385        $this->exposedDocComment = $exposedDocComment;
2386    }
2387
2388    abstract protected function getVariableTypeCode(): string;
2389
2390    abstract protected function getVariableTypeName(): string;
2391
2392    abstract protected function getFieldSynopsisDefaultLinkend(): string;
2393
2394    abstract protected function getFieldSynopsisName(): string;
2395
2396    /** @param array<string, ConstInfo> $allConstInfos */
2397    abstract protected function getFieldSynopsisValueString(array $allConstInfos): ?string;
2398
2399    abstract public function discardInfoForOldPhpVersions(?int $minimumPhpVersionIdCompatibility): void;
2400
2401    protected function addTypeToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void
2402    {
2403        $type = $this->phpDocType ?? $this->type;
2404
2405        if ($type) {
2406            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2407            $fieldsynopsisElement->appendChild($type->getTypeForDoc($doc));
2408        }
2409    }
2410
2411    /**
2412     * @return array<int, string[]>
2413     */
2414    protected function getFlagsByPhpVersion(): array
2415    {
2416        $flags = "ZEND_ACC_PUBLIC";
2417        if ($this->flags & Modifiers::PROTECTED) {
2418            $flags = "ZEND_ACC_PROTECTED";
2419        } elseif ($this->flags & Modifiers::PRIVATE) {
2420            $flags = "ZEND_ACC_PRIVATE";
2421        }
2422
2423        return [
2424            PHP_70_VERSION_ID => [$flags],
2425            PHP_80_VERSION_ID => [$flags],
2426            PHP_81_VERSION_ID => [$flags],
2427            PHP_82_VERSION_ID => [$flags],
2428            PHP_83_VERSION_ID => [$flags],
2429            PHP_84_VERSION_ID => [$flags],
2430        ];
2431    }
2432
2433    protected function getTypeCode(string $variableLikeName, string &$code): string
2434    {
2435        $variableLikeType = $this->getVariableTypeName();
2436
2437        $typeCode = "";
2438        if ($this->type) {
2439            $arginfoType = $this->type->toArginfoType();
2440            if ($arginfoType->hasClassType()) {
2441                if (count($arginfoType->classTypes) >= 2) {
2442                    foreach ($arginfoType->classTypes as $classType) {
2443                        $escapedClassName = $classType->toEscapedName();
2444                        $varEscapedClassName = $classType->toVarEscapedName();
2445                        $code .= "\tzend_string *{$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\") - 1, 1);\n";
2446                    }
2447
2448                    $classTypeCount = count($arginfoType->classTypes);
2449                    $code .= "\tzend_type_list *{$variableLikeType}_{$variableLikeName}_type_list = malloc(ZEND_TYPE_LIST_SIZE($classTypeCount));\n";
2450                    $code .= "\t{$variableLikeType}_{$variableLikeName}_type_list->num_types = $classTypeCount;\n";
2451
2452                    foreach ($arginfoType->classTypes as $k => $classType) {
2453                        $escapedClassName = $classType->toEscapedName();
2454                        $code .= "\t{$variableLikeType}_{$variableLikeName}_type_list->types[$k] = (zend_type) ZEND_TYPE_INIT_CLASS({$variableLikeType}_{$variableLikeName}_class_{$escapedClassName}, 0, 0);\n";
2455                    }
2456
2457                    $typeMaskCode = $this->type->toArginfoType()->toTypeMask();
2458
2459                    if ($this->type->isIntersection) {
2460                        $code .= "\tzend_type {$variableLikeType}_{$variableLikeName}_type = ZEND_TYPE_INIT_INTERSECTION({$variableLikeType}_{$variableLikeName}_type_list, $typeMaskCode);\n";
2461                    } else {
2462                        $code .= "\tzend_type {$variableLikeType}_{$variableLikeName}_type = ZEND_TYPE_INIT_UNION({$variableLikeType}_{$variableLikeName}_type_list, $typeMaskCode);\n";
2463                    }
2464                    $typeCode = "{$variableLikeType}_{$variableLikeName}_type";
2465                } else {
2466                    $escapedClassName = $arginfoType->classTypes[0]->toEscapedName();
2467                    $varEscapedClassName = $arginfoType->classTypes[0]->toVarEscapedName();
2468                    $code .= "\tzend_string *{$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName} = zend_string_init(\"{$escapedClassName}\", sizeof(\"{$escapedClassName}\")-1, 1);\n";
2469
2470                    $typeCode = "(zend_type) ZEND_TYPE_INIT_CLASS({$variableLikeType}_{$variableLikeName}_class_{$varEscapedClassName}, 0, " . $arginfoType->toTypeMask() . ")";
2471                }
2472            } else {
2473                $typeCode = "(zend_type) ZEND_TYPE_INIT_MASK(" . $arginfoType->toTypeMask() . ")";
2474            }
2475        } else {
2476            $typeCode = "(zend_type) ZEND_TYPE_INIT_NONE(0)";
2477        }
2478
2479        return $typeCode;
2480    }
2481
2482    /** @param array<string, ConstInfo> $allConstInfos */
2483    public function getFieldSynopsisElement(DOMDocument $doc, array $allConstInfos): DOMElement
2484    {
2485        $fieldsynopsisElement = $doc->createElement("fieldsynopsis");
2486
2487        $this->addModifiersToFieldSynopsis($doc, $fieldsynopsisElement);
2488
2489        $this->addTypeToFieldSynopsis($doc, $fieldsynopsisElement);
2490
2491        $varnameElement = $doc->createElement("varname", $this->getFieldSynopsisName());
2492        if ($this->link) {
2493            $varnameElement->setAttribute("linkend", $this->link);
2494        } else {
2495            $varnameElement->setAttribute("linkend", $this->getFieldSynopsisDefaultLinkend());
2496        }
2497
2498        $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2499        $fieldsynopsisElement->appendChild($varnameElement);
2500
2501        $valueString = $this->getFieldSynopsisValueString($allConstInfos);
2502        if ($valueString) {
2503            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2504            $initializerElement = $doc->createElement("initializer",  $valueString);
2505            $fieldsynopsisElement->appendChild($initializerElement);
2506        }
2507
2508        $fieldsynopsisElement->appendChild(new DOMText("\n    "));
2509
2510        return $fieldsynopsisElement;
2511    }
2512
2513    protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void
2514    {
2515        if ($this->flags & Modifiers::PUBLIC) {
2516            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2517            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "public"));
2518        } elseif ($this->flags & Modifiers::PROTECTED) {
2519            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2520            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "protected"));
2521        } elseif ($this->flags & Modifiers::PRIVATE) {
2522            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2523            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "private"));
2524        }
2525    }
2526
2527    /**
2528     * @param array<int, string[]> $flags
2529     * @return array<int, string[]>
2530     */
2531    protected function addFlagForVersionsAbove(array $flags, string $flag, int $minimumVersionId): array
2532    {
2533        $write = false;
2534
2535        foreach ($flags as $version => $versionFlags) {
2536            if ($version === $minimumVersionId || $write === true) {
2537                $flags[$version][] = $flag;
2538                $write = true;
2539            }
2540        }
2541
2542        return $flags;
2543    }
2544}
2545
2546class ConstInfo extends VariableLike
2547{
2548    public ConstOrClassConstName $name;
2549    public Expr $value;
2550    public bool $isDeprecated;
2551    public ?string $valueString;
2552    public ?string $cond;
2553    public ?string $cValue;
2554    public bool $isUndocumentable;
2555    public bool $isFileCacheAllowed;
2556
2557    /**
2558     * @var AttributeInfo[] $attributes
2559     */
2560    public function __construct(
2561        ConstOrClassConstName $name,
2562        int $flags,
2563        Expr $value,
2564        ?string $valueString,
2565        ?Type $type,
2566        ?Type $phpDocType,
2567        bool $isDeprecated,
2568        ?string $cond,
2569        ?string $cValue,
2570        bool $isUndocumentable,
2571        ?string $link,
2572        ?int $phpVersionIdMinimumCompatibility,
2573        array $attributes,
2574        ?ExposedDocComment $exposedDocComment,
2575        bool $isFileCacheAllowed
2576    ) {
2577        $this->name = $name;
2578        $this->value = $value;
2579        $this->valueString = $valueString;
2580        $this->isDeprecated = $isDeprecated;
2581        $this->cond = $cond;
2582        $this->cValue = $cValue;
2583        $this->isUndocumentable = $isUndocumentable;
2584        $this->isFileCacheAllowed = $isFileCacheAllowed;
2585        parent::__construct($flags, $type, $phpDocType, $link, $phpVersionIdMinimumCompatibility, $attributes, $exposedDocComment);
2586    }
2587
2588    /** @param array<string, ConstInfo> $allConstInfos */
2589    public function getValue(array $allConstInfos): EvaluatedValue
2590    {
2591        return EvaluatedValue::createFromExpression(
2592            $this->value,
2593            ($this->phpDocType ?? $this->type)->tryToSimpleType(),
2594            $this->cValue,
2595            $allConstInfos
2596        );
2597    }
2598
2599    protected function getVariableTypeName(): string
2600    {
2601        return "constant";
2602    }
2603
2604    protected function getVariableTypeCode(): string
2605    {
2606        return "const";
2607    }
2608
2609    protected function getFieldSynopsisDefaultLinkend(): string
2610    {
2611        $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString());
2612
2613        return "$className.constants." . strtolower(str_replace("_", "-", trim($this->name->getDeclarationName(), "_")));
2614    }
2615
2616    protected function getFieldSynopsisName(): string
2617    {
2618        return $this->name->__toString();
2619    }
2620
2621    /** @param array<string, ConstInfo> $allConstInfos */
2622    protected function getFieldSynopsisValueString(array $allConstInfos): ?string
2623    {
2624        $value = EvaluatedValue::createFromExpression($this->value, null, $this->cValue, $allConstInfos);
2625        if ($value->isUnknownConstValue) {
2626            return null;
2627        }
2628
2629        if ($value->originatingConsts) {
2630            return implode("\n", array_map(function (ConstInfo $const) use ($allConstInfos) {
2631                return $const->getFieldSynopsisValueString($allConstInfos);
2632            }, $value->originatingConsts));
2633        }
2634
2635        return $this->valueString;
2636    }
2637
2638    public function getPredefinedConstantTerm(DOMDocument $doc, int $indentationLevel): DOMElement {
2639        $indentation = str_repeat(" ", $indentationLevel);
2640
2641        $termElement = $doc->createElement("term");
2642
2643        $constantElement = $doc->createElement("constant");
2644        $constantElement->textContent = $this->name->__toString();
2645
2646        $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc);
2647
2648        $termElement->appendChild(new DOMText("\n$indentation "));
2649        $termElement->appendChild($constantElement);
2650        $termElement->appendChild(new DOMText("\n$indentation ("));
2651        $termElement->appendChild($typeElement);
2652        $termElement->appendChild(new DOMText(")\n$indentation"));
2653
2654        return $termElement;
2655    }
2656
2657     public function getPredefinedConstantEntry(DOMDocument $doc, int $indentationLevel): DOMElement {
2658        $indentation = str_repeat(" ", $indentationLevel);
2659
2660        $entryElement = $doc->createElement("entry");
2661
2662        $constantElement = $doc->createElement("constant");
2663        $constantElement->textContent = $this->name->__toString();
2664        $typeElement = ($this->phpDocType ?? $this->type)->getTypeForDoc($doc);
2665
2666        $entryElement->appendChild(new DOMText("\n$indentation "));
2667        $entryElement->appendChild($constantElement);
2668        $entryElement->appendChild(new DOMText("\n$indentation ("));
2669        $entryElement->appendChild($typeElement);
2670        $entryElement->appendChild(new DOMText(")\n$indentation"));
2671
2672        return $entryElement;
2673    }
2674
2675    public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
2676        $this->type = null;
2677        $this->flags &= ~Modifiers::FINAL;
2678        $this->isDeprecated = false;
2679        $this->attributes = [];
2680        $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
2681    }
2682
2683    /** @param array<string, ConstInfo> $allConstInfos */
2684    public function getDeclaration(array $allConstInfos): string
2685    {
2686        $type = $this->phpDocType ?? $this->type;
2687        $simpleType = $type ? $type->tryToSimpleType() : null;
2688        if ($simpleType && $simpleType->name === "mixed") {
2689            $simpleType = null;
2690        }
2691
2692        $value = EvaluatedValue::createFromExpression($this->value, $simpleType, $this->cValue, $allConstInfos);
2693        if ($value->isUnknownConstValue && ($simpleType === null || !$simpleType->isBuiltin)) {
2694            throw new Exception("Constant " . $this->name->__toString() . " must have a built-in PHPDoc type as the type couldn't be inferred from its value");
2695        }
2696
2697        // i.e. const NAME = UNKNOWN;, without the annotation
2698        if ($value->isUnknownConstValue && $this->cValue === null && $value->expr instanceof Expr\ConstFetch && $value->expr->name->__toString() === "UNKNOWN") {
2699            throw new Exception("Constant " . $this->name->__toString() . " must have a @cvalue annotation");
2700        }
2701
2702        $code = "";
2703
2704        if ($this->cond) {
2705            $code .= "#if {$this->cond}\n";
2706        }
2707
2708        if ($this->name->isClassConst()) {
2709            $code .= $this->getClassConstDeclaration($value, $allConstInfos);
2710        } else {
2711            $code .= $this->getGlobalConstDeclaration($value, $allConstInfos);
2712        }
2713        $code .= $this->getValueAssertion($value);
2714
2715        if ($this->cond) {
2716            $code .= "#endif\n";
2717        }
2718
2719        return $code;
2720    }
2721
2722    /** @param array<string, ConstInfo> $allConstInfos */
2723    private function getGlobalConstDeclaration(EvaluatedValue $value, array $allConstInfos): string
2724    {
2725        $constName = str_replace('\\', '\\\\', $this->name->__toString());
2726        $constValue = $value->value;
2727        $cExpr = $value->getCExpr();
2728
2729        $flags = "CONST_PERSISTENT";
2730        if (!$this->isFileCacheAllowed) {
2731            $flags .= " | CONST_NO_FILE_CACHE";
2732        }
2733        if ($this->phpVersionIdMinimumCompatibility !== null && $this->phpVersionIdMinimumCompatibility < 80000) {
2734            $flags .= " | CONST_CS";
2735        }
2736
2737        if ($this->isDeprecated) {
2738            $flags .= " | CONST_DEPRECATED";
2739        }
2740        if ($value->type->isNull()) {
2741            return "\tREGISTER_NULL_CONSTANT(\"$constName\", $flags);\n";
2742        }
2743
2744        if ($value->type->isBool()) {
2745            return "\tREGISTER_BOOL_CONSTANT(\"$constName\", " . ($cExpr ?: ($constValue ? "true" : "false")) . ", $flags);\n";
2746        }
2747
2748        if ($value->type->isInt()) {
2749            return "\tREGISTER_LONG_CONSTANT(\"$constName\", " . ($cExpr ?: (int) $constValue) . ", $flags);\n";
2750        }
2751
2752        if ($value->type->isFloat()) {
2753            return "\tREGISTER_DOUBLE_CONSTANT(\"$constName\", " . ($cExpr ?: (float) $constValue) . ", $flags);\n";
2754        }
2755
2756        if ($value->type->isString()) {
2757            return "\tREGISTER_STRING_CONSTANT(\"$constName\", " . ($cExpr ?: '"' . addslashes($constValue) . '"') . ", $flags);\n";
2758        }
2759
2760        throw new Exception("Unimplemented constant type");
2761    }
2762
2763    /** @param array<string, ConstInfo> $allConstInfos */
2764    private function getClassConstDeclaration(EvaluatedValue $value, array $allConstInfos): string
2765    {
2766        $constName = $this->name->getDeclarationName();
2767
2768        $zvalCode = $value->initializeZval("const_{$constName}_value", $allConstInfos);
2769
2770        $code = "\n" . $zvalCode;
2771
2772        $code .= "\tzend_string *const_{$constName}_name = zend_string_init_interned(\"$constName\", sizeof(\"$constName\") - 1, 1);\n";
2773        $nameCode = "const_{$constName}_name";
2774
2775        if ($this->exposedDocComment) {
2776            $commentCode = "const_{$constName}_comment";
2777            $escapedComment = $this->exposedDocComment->escape();
2778            $escapedCommentLength = $this->exposedDocComment->getLength();
2779            $code .= "\tzend_string *$commentCode = zend_string_init_interned(\"$escapedComment\", $escapedCommentLength, 1);\n";
2780        } else {
2781            $commentCode = "NULL";
2782        }
2783
2784        $php83MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_83_VERSION_ID;
2785
2786        if ($this->type && !$php83MinimumCompatibility) {
2787            $code .= "#if (PHP_VERSION_ID >= " . PHP_83_VERSION_ID . ")\n";
2788        }
2789
2790        if ($this->type) {
2791            $typeCode = $this->getTypeCode($constName, $code);
2792
2793            if (!empty($this->attributes)) {
2794                $template = "\tzend_class_constant *const_" . $this->name->getDeclarationName() . " = ";
2795            } else {
2796                $template = "\t";
2797            }
2798            $template .= "zend_declare_typed_class_constant(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode, $typeCode);\n";
2799
2800            $flagsCode = generateVersionDependentFlagCode(
2801                $template,
2802                $this->getFlagsByPhpVersion(),
2803                $this->phpVersionIdMinimumCompatibility
2804            );
2805            $code .= implode("", $flagsCode);
2806        }
2807
2808        if ($this->type && !$php83MinimumCompatibility) {
2809            $code .= "#else\n";
2810        }
2811
2812        if (!$this->type || !$php83MinimumCompatibility) {
2813            if (!empty($this->attributes)) {
2814                $template = "\tzend_class_constant *const_" . $this->name->getDeclarationName() . " = ";
2815            } else {
2816                $template = "\t";
2817            }
2818            $template .= "zend_declare_class_constant_ex(class_entry, $nameCode, &const_{$constName}_value, %s, $commentCode);\n";
2819            $flagsCode = generateVersionDependentFlagCode(
2820                $template,
2821                $this->getFlagsByPhpVersion(),
2822                $this->phpVersionIdMinimumCompatibility
2823            );
2824            $code .= implode("", $flagsCode);
2825        }
2826
2827        if ($this->type && !$php83MinimumCompatibility) {
2828            $code .= "#endif\n";
2829        }
2830
2831        $code .= "\tzend_string_release(const_{$constName}_name);\n";
2832
2833        return $code;
2834    }
2835
2836    private function getValueAssertion(EvaluatedValue $value): string
2837    {
2838        if ($value->isUnknownConstValue || $value->originatingConsts || $this->cValue === null) {
2839            return "";
2840        }
2841
2842        $cExpr = $value->getCExpr();
2843        $constValue = $value->value;
2844
2845        if ($value->type->isNull()) {
2846            return "\tZEND_ASSERT($cExpr == NULL);\n";
2847        }
2848
2849        if ($value->type->isBool()) {
2850            $cValue = $constValue ? "true" : "false";
2851            return "\tZEND_ASSERT($cExpr == $cValue);\n";
2852        }
2853
2854        if ($value->type->isInt()) {
2855            $cValue = (int) $constValue;
2856            return "\tZEND_ASSERT($cExpr == $cValue);\n";
2857        }
2858
2859        if ($value->type->isFloat()) {
2860            $cValue = (float) $constValue;
2861            return "\tZEND_ASSERT($cExpr == $cValue);\n";
2862        }
2863
2864        if ($value->type->isString()) {
2865            $cValue = '"' . addslashes($constValue) . '"';
2866            return "\tZEND_ASSERT(strcmp($cExpr, $cValue) == 0);\n";
2867        }
2868
2869        throw new Exception("Unimplemented constant type");
2870    }
2871
2872    /**
2873     * @return array<int, string[]>
2874     */
2875    protected function getFlagsByPhpVersion(): array
2876    {
2877        $flags = parent::getFlagsByPhpVersion();
2878
2879        if ($this->isDeprecated) {
2880            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID);
2881        }
2882
2883        foreach ($this->attributes as $attr) {
2884            if ($attr->class === "Deprecated") {
2885                $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_DEPRECATED", PHP_80_VERSION_ID);
2886                break;
2887            }
2888        }
2889
2890        if ($this->flags & Modifiers::FINAL) {
2891            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_FINAL", PHP_81_VERSION_ID);
2892        }
2893
2894        return $flags;
2895    }
2896
2897    protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void
2898    {
2899        parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement);
2900
2901        if ($this->flags & Modifiers::FINAL) {
2902            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2903            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "final"));
2904        }
2905
2906        $fieldsynopsisElement->appendChild(new DOMText("\n     "));
2907        $fieldsynopsisElement->appendChild($doc->createElement("modifier", "const"));
2908    }
2909}
2910
2911class PropertyInfo extends VariableLike
2912{
2913    public int $classFlags;
2914    public PropertyName $name;
2915    public ?Expr $defaultValue;
2916    public ?string $defaultValueString;
2917    public bool $isDocReadonly;
2918    public bool $isVirtual;
2919
2920    // Map possible variable names to the known string constant, see
2921    // ZEND_KNOWN_STRINGS
2922    private const PHP_80_KNOWN = [
2923        "file" => "ZEND_STR_FILE",
2924        "line" => "ZEND_STR_LINE",
2925        "function" => "ZEND_STR_FUNCTION",
2926        "class" => "ZEND_STR_CLASS",
2927        "object" => "ZEND_STR_OBJECT",
2928        "type" => "ZEND_STR_TYPE",
2929        // ZEND_STR_OBJECT_OPERATOR and ZEND_STR_PAAMAYIM_NEKUDOTAYIM are
2930        // not valid variable names
2931        "args" => "ZEND_STR_ARGS",
2932        "unknown" => "ZEND_STR_UNKNOWN",
2933        "eval" => "ZEND_STR_EVAL",
2934        "include" => "ZEND_STR_INCLUDE",
2935        "require" => "ZEND_STR_REQUIRE",
2936        "include_once" => "ZEND_STR_INCLUDE_ONCE",
2937        "require_once" => "ZEND_STR_REQUIRE_ONCE",
2938        "scalar" => "ZEND_STR_SCALAR",
2939        "error_reporting" => "ZEND_STR_ERROR_REPORTING",
2940        "static" => "ZEND_STR_STATIC",
2941        // ZEND_STR_THIS cannot be used since $this cannot be reassigned
2942        "value" => "ZEND_STR_VALUE",
2943        "key" => "ZEND_STR_KEY",
2944        "__invoke" => "ZEND_STR_MAGIC_INVOKE",
2945        "previous" => "ZEND_STR_PREVIOUS",
2946        "code" => "ZEND_STR_CODE",
2947        "message" => "ZEND_STR_MESSAGE",
2948        "severity" => "ZEND_STR_SEVERITY",
2949        "string" => "ZEND_STR_STRING",
2950        "trace" => "ZEND_STR_TRACE",
2951        "scheme" => "ZEND_STR_SCHEME",
2952        "host" => "ZEND_STR_HOST",
2953        "port" => "ZEND_STR_PORT",
2954        "user" => "ZEND_STR_USER",
2955        "pass" => "ZEND_STR_PASS",
2956        "path" => "ZEND_STR_PATH",
2957        "query" => "ZEND_STR_QUERY",
2958        "fragment" => "ZEND_STR_FRAGMENT",
2959        "NULL" => "ZEND_STR_NULL",
2960        "boolean" => "ZEND_STR_BOOLEAN",
2961        "integer" => "ZEND_STR_INTEGER",
2962        "double" => "ZEND_STR_DOUBLE",
2963        "array" => "ZEND_STR_ARRAY",
2964        "resource" => "ZEND_STR_RESOURCE",
2965        // ZEND_STR_CLOSED_RESOURCE has a space in it
2966        "name" => "ZEND_STR_NAME",
2967        // ZEND_STR_ARGV and ZEND_STR_ARGC are superglobals that wouldn't be
2968        // variable names
2969        "Array" => "ZEND_STR_ARRAY_CAPITALIZED",
2970        "bool" => "ZEND_STR_BOOL",
2971        "int" => "ZEND_STR_INT",
2972        "float" => "ZEND_STR_FLOAT",
2973        "callable" => "ZEND_STR_CALLABLE",
2974        "iterable" => "ZEND_STR_ITERABLE",
2975        "void" => "ZEND_STR_VOID",
2976        "false" => "ZEND_STR_FALSE",
2977        "null" => "ZEND_STR_NULL_LOWERCASE",
2978        "mixed" => "ZEND_STR_MIXED",
2979    ];
2980
2981    // NEW in 8.1
2982    private const PHP_81_KNOWN = [
2983        "Unknown" => "ZEND_STR_UNKNOWN_CAPITALIZED",
2984        "never" => "ZEND_STR_NEVER",
2985        "__sleep" => "ZEND_STR_SLEEP",
2986        "__wakeup" => "ZEND_STR_WAKEUP",
2987        "cases" => "ZEND_STR_CASES",
2988        "from" => "ZEND_STR_FROM",
2989        "tryFrom" => "ZEND_STR_TRYFROM",
2990        "tryfrom" => "ZEND_STR_TRYFROM_LOWERCASE",
2991        // Omit ZEND_STR_AUTOGLOBAL_(SERVER|ENV|REQUEST)
2992    ];
2993
2994    // NEW in 8.2
2995    private const PHP_82_KNOWN = [
2996        "true" => "ZEND_STR_TRUE",
2997        "Traversable" => "ZEND_STR_TRAVERSABLE",
2998        "count" => "ZEND_STR_COUNT",
2999        "SensitiveParameter" => "ZEND_STR_SENSITIVEPARAMETER",
3000    ];
3001
3002    // Only new string in 8.3 is ZEND_STR_CONST_EXPR_PLACEHOLDER which is
3003    // not a valid variable name ("[constant expression]")
3004
3005    // NEW in 8.4
3006    private const PHP_84_KNOWN = [
3007        "exit" => "ZEND_STR_EXIT",
3008        "Deprecated" => "ZEND_STR_DEPRECATED_CAPITALIZED",
3009        "since" => "ZEND_STR_SINCE",
3010        "get" => "ZEND_STR_GET",
3011        "set" => "ZEND_STR_SET",
3012    ];
3013
3014    /**
3015     * @var AttributeInfo[] $attributes
3016     */
3017    public function __construct(
3018        PropertyName $name,
3019        int $classFlags,
3020        int $flags,
3021        ?Type $type,
3022        ?Type $phpDocType,
3023        ?Expr $defaultValue,
3024        ?string $defaultValueString,
3025        bool $isDocReadonly,
3026        bool $isVirtual,
3027        ?string $link,
3028        ?int $phpVersionIdMinimumCompatibility,
3029        array $attributes,
3030        ?ExposedDocComment $exposedDocComment
3031    ) {
3032        $this->name = $name;
3033        $this->classFlags = $classFlags;
3034        $this->defaultValue = $defaultValue;
3035        $this->defaultValueString = $defaultValueString;
3036        $this->isDocReadonly = $isDocReadonly;
3037        $this->isVirtual = $isVirtual;
3038        parent::__construct($flags, $type, $phpDocType, $link, $phpVersionIdMinimumCompatibility, $attributes, $exposedDocComment);
3039    }
3040
3041    protected function getVariableTypeCode(): string
3042    {
3043        return "property";
3044    }
3045
3046    protected function getVariableTypeName(): string
3047    {
3048        return "property";
3049    }
3050
3051    protected function getFieldSynopsisDefaultLinkend(): string
3052    {
3053        $className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString());
3054
3055        return "$className.props." . strtolower(str_replace("_", "-", trim($this->name->getDeclarationName(), "_")));
3056    }
3057
3058    protected function getFieldSynopsisName(): string
3059    {
3060        return $this->name->getDeclarationName();
3061    }
3062
3063    /** @param array<string, ConstInfo> $allConstInfos */
3064    protected function getFieldSynopsisValueString(array $allConstInfos): ?string
3065    {
3066        return $this->defaultValueString;
3067    }
3068
3069    public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
3070        $this->type = null;
3071        $this->flags &= ~Modifiers::READONLY;
3072        $this->attributes = [];
3073        $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
3074    }
3075
3076    /** @param array<string, ConstInfo> $allConstInfos */
3077    public function getDeclaration(array $allConstInfos): string {
3078        $code = "\n";
3079
3080        $propertyName = $this->name->getDeclarationName();
3081
3082        if ($this->defaultValue === null) {
3083            $defaultValue = EvaluatedValue::null();
3084        } else {
3085            $defaultValue = EvaluatedValue::createFromExpression($this->defaultValue, null, null, $allConstInfos);
3086            if ($defaultValue->isUnknownConstValue || ($defaultValue->originatingConsts && $defaultValue->getCExpr() === null)) {
3087                echo "Skipping code generation for property $this->name, because it has an unknown constant default value\n";
3088                return "";
3089            }
3090        }
3091
3092        $zvalName = "property_{$propertyName}_default_value";
3093        if ($this->defaultValue === null && $this->type !== null) {
3094            $code .= "\tzval $zvalName;\n\tZVAL_UNDEF(&$zvalName);\n";
3095        } else {
3096            $code .= $defaultValue->initializeZval($zvalName);
3097        }
3098
3099        [$stringInit, $nameCode, $stringRelease] = $this->getString($propertyName);
3100        $code .= $stringInit;
3101
3102        if ($this->exposedDocComment) {
3103            $commentCode = "property_{$propertyName}_comment";
3104            $escapedComment = $this->exposedDocComment->escape();
3105            $escapedCommentLength = $this->exposedDocComment->getLength();
3106            $code .= "\tzend_string *$commentCode = zend_string_init_interned(\"$escapedComment\", $escapedCommentLength, 1);\n";
3107        } else {
3108            $commentCode = "NULL";
3109        }
3110
3111        if (!empty($this->attributes)) {
3112            $template = "\tzend_property_info *property_" . $this->name->getDeclarationName() . " = ";
3113        } else {
3114            $template = "\t";
3115        }
3116
3117        if ($this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID) {
3118            $typeCode = $this->getTypeCode($propertyName, $code);
3119            $template .= "zend_declare_typed_property(class_entry, $nameCode, &$zvalName, %s, $commentCode, $typeCode);\n";
3120        } else {
3121            $template .= "zend_declare_property_ex(class_entry, $nameCode, &$zvalName, %s, $commentCode);\n";
3122        }
3123
3124        $flagsCode = generateVersionDependentFlagCode(
3125            $template,
3126            $this->getFlagsByPhpVersion(),
3127            $this->phpVersionIdMinimumCompatibility
3128        );
3129        $code .= implode("", $flagsCode);
3130
3131        $code .= $stringRelease;
3132
3133        return $code;
3134    }
3135
3136    /**
3137     * Get an array of three strings:
3138     *   - declaration of zend_string, if needed, or empty otherwise
3139     *   - usage of that zend_string, or usage with ZSTR_KNOWN()
3140     *   - freeing the zend_string, if needed
3141     *
3142     * @param string $propName
3143     * @return string[]
3144     */
3145    private function getString(string $propName): array {
3146        // Generally strings will not be known
3147        $nameCode = "property_{$propName}_name";
3148        $result = [
3149            "\tzend_string *$nameCode = zend_string_init(\"$propName\", sizeof(\"$propName\") - 1, 1);\n",
3150            $nameCode,
3151            "\tzend_string_release($nameCode);\n"
3152        ];
3153        // If not set, use the current latest version
3154        $allVersions = ALL_PHP_VERSION_IDS;
3155        $minPhp = $phpVersionIdMinimumCompatibility ?? end($allVersions);
3156        if ($minPhp < PHP_80_VERSION_ID) {
3157            // No known strings in 7.0
3158            return $result;
3159        }
3160        $include = self::PHP_80_KNOWN;
3161        switch ($minPhp) {
3162            case PHP_84_VERSION_ID:
3163                $include = array_merge($include, self::PHP_84_KNOWN);
3164                // Intentional fall through
3165
3166            case PHP_83_VERSION_ID:
3167            case PHP_82_VERSION_ID:
3168                $include = array_merge($include, self::PHP_82_KNOWN);
3169                // Intentional fall through
3170
3171            case PHP_81_VERSION_ID:
3172                $include = array_merge($include, self::PHP_81_KNOWN);
3173                break;
3174        }
3175        if (array_key_exists($propName,$include)) {
3176            $knownStr = $include[$propName];
3177            return [
3178                '',
3179                "ZSTR_KNOWN($knownStr)",
3180                '',
3181            ];
3182        }
3183        return $result;
3184    }
3185
3186    /**
3187     * @return array<int, string[]>
3188     */
3189    protected function getFlagsByPhpVersion(): array
3190    {
3191        $flags = parent::getFlagsByPhpVersion();
3192
3193        if ($this->flags & Modifiers::STATIC) {
3194            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_STATIC", PHP_70_VERSION_ID);
3195        }
3196
3197        if ($this->flags & Modifiers::READONLY) {
3198            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_81_VERSION_ID);
3199        } elseif ($this->classFlags & Modifiers::READONLY) {
3200            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_READONLY", PHP_82_VERSION_ID);
3201        }
3202
3203        if ($this->isVirtual) {
3204            $flags = $this->addFlagForVersionsAbove($flags, "ZEND_ACC_VIRTUAL", PHP_84_VERSION_ID);
3205        }
3206
3207        return $flags;
3208    }
3209
3210    protected function addModifiersToFieldSynopsis(DOMDocument $doc, DOMElement $fieldsynopsisElement): void
3211    {
3212        parent::addModifiersToFieldSynopsis($doc, $fieldsynopsisElement);
3213
3214        if ($this->flags & Modifiers::STATIC) {
3215            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
3216            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "static"));
3217        }
3218
3219        if ($this->flags & Modifiers::READONLY || $this->isDocReadonly) {
3220            $fieldsynopsisElement->appendChild(new DOMText("\n     "));
3221            $fieldsynopsisElement->appendChild($doc->createElement("modifier", "readonly"));
3222        }
3223    }
3224
3225    public function __clone()
3226    {
3227        if ($this->type) {
3228            $this->type = clone $this->type;
3229        }
3230        foreach ($this->attributes as $key => $attribute) {
3231            $this->attributes[$key] = clone $attribute;
3232        }
3233        if ($this->exposedDocComment) {
3234            $this->exposedDocComment = clone $this->exposedDocComment;
3235        }
3236    }
3237}
3238
3239class EnumCaseInfo {
3240    public string $name;
3241    public ?Expr $value;
3242
3243    public function __construct(string $name, ?Expr $value) {
3244        $this->name = $name;
3245        $this->value = $value;
3246    }
3247
3248    /** @param array<string, ConstInfo> $allConstInfos */
3249    public function getDeclaration(array $allConstInfos): string {
3250        $escapedName = addslashes($this->name);
3251        if ($this->value === null) {
3252            $code = "\n\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", NULL);\n";
3253        } else {
3254            $value = EvaluatedValue::createFromExpression($this->value, null, null, $allConstInfos);
3255
3256            $zvalName = "enum_case_{$escapedName}_value";
3257            $code = "\n" . $value->initializeZval($zvalName);
3258            $code .= "\tzend_enum_add_case_cstr(class_entry, \"$escapedName\", &$zvalName);\n";
3259        }
3260
3261        return $code;
3262    }
3263}
3264
3265class AttributeInfo {
3266    public string $class;
3267    /** @var \PhpParser\Node\Arg[] */
3268    public array $args;
3269
3270    /** @param \PhpParser\Node\Arg[] $args */
3271    public function __construct(string $class, array $args) {
3272        $this->class = $class;
3273        $this->args = $args;
3274    }
3275
3276    /** @param array<string, ConstInfo> $allConstInfos */
3277    public function generateCode(string $invocation, string $nameSuffix, array $allConstInfos, ?int $phpVersionIdMinimumCompatibility): string {
3278        $php82MinimumCompatibility = $phpVersionIdMinimumCompatibility === null || $phpVersionIdMinimumCompatibility >= PHP_82_VERSION_ID;
3279        $php84MinimumCompatibility = $phpVersionIdMinimumCompatibility === null || $phpVersionIdMinimumCompatibility >= PHP_84_VERSION_ID;
3280        /* see ZEND_KNOWN_STRINGS in Zend/strings.h */
3281        $knowns = [
3282            "message" => "ZEND_STR_MESSAGE",
3283        ];
3284        if ($php82MinimumCompatibility) {
3285            $knowns["SensitiveParameter"] = "ZEND_STR_SENSITIVEPARAMETER";
3286        }
3287        if ($php84MinimumCompatibility) {
3288            $knowns["Deprecated"] = "ZEND_STR_DEPRECATED_CAPITALIZED";
3289            $knowns["since"] = "ZEND_STR_SINCE";
3290        }
3291
3292        $code = "\n";
3293        $escapedAttributeName = strtr($this->class, '\\', '_');
3294        if (isset($knowns[$escapedAttributeName])) {
3295            $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, ZSTR_KNOWN({$knowns[$escapedAttributeName]}), " . count($this->args) . ");\n";
3296        } else {
3297            $code .= "\tzend_string *attribute_name_{$escapedAttributeName}_$nameSuffix = zend_string_init_interned(\"" . addcslashes($this->class, "\\") . "\", sizeof(\"" . addcslashes($this->class, "\\") . "\") - 1, 1);\n";
3298            $code .= "\t" . ($this->args ? "zend_attribute *attribute_{$escapedAttributeName}_$nameSuffix = " : "") . "$invocation, attribute_name_{$escapedAttributeName}_$nameSuffix, " . count($this->args) . ");\n";
3299            $code .= "\tzend_string_release(attribute_name_{$escapedAttributeName}_$nameSuffix);\n";
3300        }
3301        foreach ($this->args as $i => $arg) {
3302            $value = EvaluatedValue::createFromExpression($arg->value, null, null, $allConstInfos);
3303            $zvalName = "attribute_{$escapedAttributeName}_{$nameSuffix}_arg$i";
3304            $code .= $value->initializeZval($zvalName);
3305            $code .= "\tZVAL_COPY_VALUE(&attribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].value, &$zvalName);\n";
3306            if ($arg->name) {
3307                if (isset($knowns[$arg->name->name])) {
3308                    $code .= "\tattribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].name = ZSTR_KNOWN({$knowns[$arg->name->name]});\n";
3309                } else {
3310                    $code .= "\tattribute_{$escapedAttributeName}_{$nameSuffix}->args[$i].name = zend_string_init_interned(\"{$arg->name->name}\", sizeof(\"{$arg->name->name}\") - 1, 1);\n";
3311                }
3312            }
3313        }
3314        return $code;
3315    }
3316}
3317
3318class ClassInfo {
3319    public Name $name;
3320    public int $flags;
3321    public string $type;
3322    public ?string $alias;
3323    public ?SimpleType $enumBackingType;
3324    public bool $isDeprecated;
3325    public bool $isStrictProperties;
3326    /** @var AttributeInfo[] */
3327    public array $attributes;
3328    public ?ExposedDocComment $exposedDocComment;
3329    public bool $isNotSerializable;
3330    /** @var Name[] */
3331    public array $extends;
3332    /** @var Name[] */
3333    public array $implements;
3334    /** @var ConstInfo[] */
3335    public array $constInfos;
3336    /** @var PropertyInfo[] */
3337    public array $propertyInfos;
3338    /** @var FuncInfo[] */
3339    public array $funcInfos;
3340    /** @var EnumCaseInfo[] */
3341    public array $enumCaseInfos;
3342    public ?string $cond;
3343    public ?int $phpVersionIdMinimumCompatibility;
3344    public bool $isUndocumentable;
3345
3346    /**
3347     * @param AttributeInfo[] $attributes
3348     * @param Name[] $extends
3349     * @param Name[] $implements
3350     * @param ConstInfo[] $constInfos
3351     * @param PropertyInfo[] $propertyInfos
3352     * @param FuncInfo[] $funcInfos
3353     * @param EnumCaseInfo[] $enumCaseInfos
3354     */
3355    public function __construct(
3356        Name $name,
3357        int $flags,
3358        string $type,
3359        ?string $alias,
3360        ?SimpleType $enumBackingType,
3361        bool $isDeprecated,
3362        bool $isStrictProperties,
3363        array $attributes,
3364        ?ExposedDocComment $exposedDocComment,
3365        bool $isNotSerializable,
3366        array $extends,
3367        array $implements,
3368        array $constInfos,
3369        array $propertyInfos,
3370        array $funcInfos,
3371        array $enumCaseInfos,
3372        ?string $cond,
3373        ?int $minimumPhpVersionIdCompatibility,
3374        bool $isUndocumentable
3375    ) {
3376        $this->name = $name;
3377        $this->flags = $flags;
3378        $this->type = $type;
3379        $this->alias = $alias;
3380        $this->enumBackingType = $enumBackingType;
3381        $this->isDeprecated = $isDeprecated;
3382        $this->isStrictProperties = $isStrictProperties;
3383        $this->attributes = $attributes;
3384        $this->exposedDocComment = $exposedDocComment;
3385        $this->isNotSerializable = $isNotSerializable;
3386        $this->extends = $extends;
3387        $this->implements = $implements;
3388        $this->constInfos = $constInfos;
3389        $this->propertyInfos = $propertyInfos;
3390        $this->funcInfos = $funcInfos;
3391        $this->enumCaseInfos = $enumCaseInfos;
3392        $this->cond = $cond;
3393        $this->phpVersionIdMinimumCompatibility = $minimumPhpVersionIdCompatibility;
3394        $this->isUndocumentable = $isUndocumentable;
3395    }
3396
3397    /** @param array<string, ConstInfo> $allConstInfos */
3398    public function getRegistration(array $allConstInfos): string
3399    {
3400        $params = [];
3401        foreach ($this->extends as $extends) {
3402            $params[] = "zend_class_entry *class_entry_" . implode("_", $extends->getParts());
3403        }
3404        foreach ($this->implements as $implements) {
3405            $params[] = "zend_class_entry *class_entry_" . implode("_", $implements->getParts());
3406        }
3407
3408        $escapedName = implode("_", $this->name->getParts());
3409
3410        $code = '';
3411
3412        $php80MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_80_VERSION_ID;
3413        $php81MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_81_VERSION_ID;
3414        $php84MinimumCompatibility = $this->phpVersionIdMinimumCompatibility === null || $this->phpVersionIdMinimumCompatibility >= PHP_84_VERSION_ID;
3415
3416        if ($this->type === "enum" && !$php81MinimumCompatibility) {
3417            $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n";
3418        }
3419
3420        if ($this->cond) {
3421            $code .= "#if {$this->cond}\n";
3422        }
3423
3424        $code .= "static zend_class_entry *register_class_$escapedName(" . (empty($params) ? "void" : implode(", ", $params)) . ")\n";
3425
3426        $code .= "{\n";
3427
3428        $flagCodes = generateVersionDependentFlagCode("%s", $this->getFlagsByPhpVersion(), $this->phpVersionIdMinimumCompatibility);
3429        $flags = implode("", $flagCodes);
3430
3431        $classMethods = ($this->funcInfos === []) ? 'NULL' : "class_{$escapedName}_methods";
3432        if ($this->type === "enum") {
3433            $name = addslashes((string) $this->name);
3434            $backingType = $this->enumBackingType
3435                ? $this->enumBackingType->toTypeCode() : "IS_UNDEF";
3436            $code .= "\tzend_class_entry *class_entry = zend_register_internal_enum(\"$name\", $backingType, $classMethods);\n";
3437            if ($flags !== "") {
3438                $code .= "\tclass_entry->ce_flags |= $flags\n";
3439            }
3440        } else {
3441            $code .= "\tzend_class_entry ce, *class_entry;\n\n";
3442            if (count($this->name->getParts()) > 1) {
3443                $className = $this->name->getLast();
3444                $namespace = addslashes((string) $this->name->slice(0, -1));
3445
3446                $code .= "\tINIT_NS_CLASS_ENTRY(ce, \"$namespace\", \"$className\", $classMethods);\n";
3447            } else {
3448                $code .= "\tINIT_CLASS_ENTRY(ce, \"$this->name\", $classMethods);\n";
3449            }
3450
3451            if ($this->type === "class" || $this->type === "trait") {
3452                if (!$php84MinimumCompatibility) {
3453                    $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n";
3454                }
3455
3456                $code .= "\tclass_entry = zend_register_internal_class_with_flags(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ", " . ($flags ?: 0) . ");\n";
3457
3458                if (!$php84MinimumCompatibility) {
3459                    $code .= "#else\n";
3460
3461                    $code .= "\tclass_entry = zend_register_internal_class_ex(&ce, " . (isset($this->extends[0]) ? "class_entry_" . str_replace("\\", "_", $this->extends[0]->toString()) : "NULL") . ");\n";
3462                    if ($flags !== "") {
3463                        $code .= "\tclass_entry->ce_flags |= $flags;\n";
3464                    }
3465                    $code .= "#endif\n";
3466                }
3467            } else {
3468                $code .= "\tclass_entry = zend_register_internal_interface(&ce);\n";
3469                if ($flags !== "") {
3470                    $code .= "\tclass_entry->ce_flags |= $flags\n";
3471                }
3472            }
3473        }
3474
3475        if ($this->exposedDocComment) {
3476            if (!$php84MinimumCompatibility) {
3477                $code .= "#if (PHP_VERSION_ID >= " . PHP_84_VERSION_ID . ")\n";
3478            }
3479
3480            $code .= "\tclass_entry->doc_comment = zend_string_init_interned(\"" . $this->exposedDocComment->escape() . "\", " . $this->exposedDocComment->getLength() . ", 1);\n";
3481
3482            if (!$php84MinimumCompatibility) {
3483                $code .= "#endif\n";
3484            }
3485        }
3486
3487        $implements = array_map(
3488            function (Name $item) {
3489                return "class_entry_" . implode("_", $item->getParts());
3490            },
3491            $this->type === "interface" ? $this->extends : $this->implements
3492        );
3493
3494        if (!empty($implements)) {
3495            $code .= "\tzend_class_implements(class_entry, " . count($implements) . ", " . implode(", ", $implements) . ");\n";
3496        }
3497
3498        if ($this->alias) {
3499            $code .= "\tzend_register_class_alias(\"" . str_replace("\\", "\\\\", $this->alias) . "\", class_entry);\n";
3500        }
3501
3502        foreach ($this->constInfos as $const) {
3503            $code .= $const->getDeclaration($allConstInfos);
3504        }
3505
3506        foreach ($this->enumCaseInfos as $enumCase) {
3507            $code .= $enumCase->getDeclaration($allConstInfos);
3508        }
3509
3510        foreach ($this->propertyInfos as $property) {
3511            $code .= $property->getDeclaration($allConstInfos);
3512        }
3513
3514        if (!empty($this->attributes)) {
3515            if (!$php80MinimumCompatibility) {
3516                $code .= "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")";
3517            }
3518
3519            foreach ($this->attributes as $key => $attribute) {
3520                $code .= $attribute->generateCode(
3521                    "zend_add_class_attribute(class_entry",
3522                    "class_{$escapedName}_$key",
3523                    $allConstInfos,
3524                    $this->phpVersionIdMinimumCompatibility
3525                );
3526            }
3527
3528            if (!$php80MinimumCompatibility) {
3529                $code .= "#endif\n";
3530            }
3531        }
3532
3533        if ($attributeInitializationCode = generateConstantAttributeInitialization($this->constInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) {
3534            if (!$php80MinimumCompatibility) {
3535                $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")";
3536            }
3537
3538            $code .= "\n" . $attributeInitializationCode;
3539
3540            if (!$php80MinimumCompatibility) {
3541                $code .= "#endif\n";
3542            }
3543        }
3544
3545        if ($attributeInitializationCode = generatePropertyAttributeInitialization($this->propertyInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility)) {
3546            if (!$php80MinimumCompatibility) {
3547                $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")";
3548            }
3549
3550            $code .= "\n" . $attributeInitializationCode;
3551
3552            if (!$php80MinimumCompatibility) {
3553                $code .= "#endif\n";
3554            }
3555        }
3556
3557        if ($attributeInitializationCode = generateFunctionAttributeInitialization($this->funcInfos, $allConstInfos, $this->phpVersionIdMinimumCompatibility, $this->cond)) {
3558            if (!$php80MinimumCompatibility) {
3559                $code .= "#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")\n";
3560            }
3561
3562            $code .= "\n" . $attributeInitializationCode;
3563
3564            if (!$php80MinimumCompatibility) {
3565                $code .= "#endif\n";
3566            }
3567        }
3568
3569        $code .= "\n\treturn class_entry;\n";
3570
3571        $code .= "}\n";
3572
3573        if ($this->cond) {
3574            $code .= "#endif\n";
3575        }
3576
3577        if ($this->type === "enum" && !$php81MinimumCompatibility) {
3578            $code .= "#endif\n";
3579        }
3580
3581        return $code;
3582    }
3583
3584    /**
3585     * @return array<int, string[]>
3586     */
3587    private function getFlagsByPhpVersion(): array
3588    {
3589        $php70Flags = [];
3590
3591        if ($this->type === "trait") {
3592            $php70Flags[] = "ZEND_ACC_TRAIT";
3593        }
3594
3595        if ($this->flags & Modifiers::FINAL) {
3596            $php70Flags[] = "ZEND_ACC_FINAL";
3597        }
3598
3599        if ($this->flags & Modifiers::ABSTRACT) {
3600            $php70Flags[] = "ZEND_ACC_ABSTRACT";
3601        }
3602
3603        if ($this->isDeprecated) {
3604            $php70Flags[] = "ZEND_ACC_DEPRECATED";
3605        }
3606
3607        $php80Flags = $php70Flags;
3608
3609        if ($this->isStrictProperties) {
3610            $php80Flags[] = "ZEND_ACC_NO_DYNAMIC_PROPERTIES";
3611        }
3612
3613        $php81Flags = $php80Flags;
3614
3615        if ($this->isNotSerializable) {
3616            $php81Flags[] = "ZEND_ACC_NOT_SERIALIZABLE";
3617        }
3618
3619        $php82Flags = $php81Flags;
3620
3621        if ($this->flags & Modifiers::READONLY) {
3622            $php82Flags[] = "ZEND_ACC_READONLY_CLASS";
3623        }
3624
3625        foreach ($this->attributes as $attr) {
3626            if ($attr->class === "AllowDynamicProperties") {
3627                $php82Flags[] = "ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES";
3628                break;
3629            }
3630        }
3631
3632        $php83Flags = $php82Flags;
3633        $php84Flags = $php83Flags;
3634
3635        return [
3636            PHP_70_VERSION_ID => $php70Flags,
3637            PHP_80_VERSION_ID => $php80Flags,
3638            PHP_81_VERSION_ID => $php81Flags,
3639            PHP_82_VERSION_ID => $php82Flags,
3640            PHP_83_VERSION_ID => $php83Flags,
3641            PHP_84_VERSION_ID => $php84Flags,
3642        ];
3643    }
3644
3645    public function discardInfoForOldPhpVersions(?int $phpVersionIdMinimumCompatibility): void {
3646        $this->attributes = [];
3647        $this->flags &= ~Modifiers::READONLY;
3648        $this->exposedDocComment = null;
3649        $this->isStrictProperties = false;
3650        $this->isNotSerializable = false;
3651
3652        foreach ($this->propertyInfos as $propertyInfo) {
3653            $propertyInfo->discardInfoForOldPhpVersions($phpVersionIdMinimumCompatibility);
3654        }
3655        $this->phpVersionIdMinimumCompatibility = $phpVersionIdMinimumCompatibility;
3656    }
3657
3658    /**
3659     * @param array<string, ClassInfo> $classMap
3660     * @param array<string, ConstInfo> $allConstInfos
3661     * @param iterable<ConstInfo> $allConstInfo
3662     */
3663    public function getClassSynopsisDocument(array $classMap, array $allConstInfos): ?string {
3664
3665        $doc = new DOMDocument();
3666        $doc->formatOutput = true;
3667        $classSynopsis = $this->getClassSynopsisElement($doc, $classMap, $allConstInfos);
3668        if (!$classSynopsis) {
3669            return null;
3670        }
3671
3672        $doc->appendChild($classSynopsis);
3673
3674        return $doc->saveXML();
3675    }
3676
3677    /**
3678     * @param array<string, ClassInfo> $classMap
3679     * @param array<string, ConstInfo> $allConstInfos
3680     */
3681    public function getClassSynopsisElement(DOMDocument $doc, array $classMap, array $allConstInfos): ?DOMElement {
3682
3683        $classSynopsis = $doc->createElement("classsynopsis");
3684        $classSynopsis->setAttribute("class", $this->type === "interface" ? "interface" : "class");
3685
3686        $exceptionOverride = $this->type === "class" && $this->isException($classMap) ? "exception" : null;
3687        $ooElement = self::createOoElement($doc, $this, $exceptionOverride, true, null, 4);
3688        if (!$ooElement) {
3689            return null;
3690        }
3691        $classSynopsis->appendChild(new DOMText("\n    "));
3692        $classSynopsis->appendChild($ooElement);
3693
3694        foreach ($this->extends as $k => $parent) {
3695            $parentInfo = $classMap[$parent->toString()] ?? null;
3696            if ($parentInfo === null) {
3697                throw new Exception("Missing parent class " . $parent->toString());
3698            }
3699
3700            $ooElement = self::createOoElement(
3701                $doc,
3702                $parentInfo,
3703                null,
3704                false,
3705                $k === 0 ? "extends" : null,
3706                4
3707            );
3708            if (!$ooElement) {
3709                return null;
3710            }
3711
3712            $classSynopsis->appendChild(new DOMText("\n\n    "));
3713            $classSynopsis->appendChild($ooElement);
3714        }
3715
3716        foreach ($this->implements as $k => $interface) {
3717            $interfaceInfo = $classMap[$interface->toString()] ?? null;
3718            if (!$interfaceInfo) {
3719                throw new Exception("Missing implemented interface " . $interface->toString());
3720            }
3721
3722            $ooElement = self::createOoElement($doc, $interfaceInfo, null, false, $k === 0 ? "implements" : null, 4);
3723            if (!$ooElement) {
3724                return null;
3725            }
3726            $classSynopsis->appendChild(new DOMText("\n\n    "));
3727            $classSynopsis->appendChild($ooElement);
3728        }
3729
3730        /** @var array<string, Name> $parentsWithInheritedConstants */
3731        $parentsWithInheritedConstants = [];
3732        /** @var array<string, Name> $parentsWithInheritedProperties */
3733        $parentsWithInheritedProperties = [];
3734        /** @var array<int, array{name: Name, types: int[]}> $parentsWithInheritedMethods */
3735        $parentsWithInheritedMethods = [];
3736
3737        $this->collectInheritedMembers(
3738            $parentsWithInheritedConstants,
3739            $parentsWithInheritedProperties,
3740            $parentsWithInheritedMethods,
3741            $this->hasConstructor(),
3742            $classMap
3743        );
3744
3745        $this->appendInheritedMemberSectionToClassSynopsis(
3746            $doc,
3747            $classSynopsis,
3748            $parentsWithInheritedConstants,
3749            "&Constants;",
3750            "&InheritedConstants;"
3751        );
3752
3753        if (!empty($this->constInfos)) {
3754            $classSynopsis->appendChild(new DOMText("\n\n    "));
3755            $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Constants;");
3756            $classSynopsisInfo->setAttribute("role", "comment");
3757            $classSynopsis->appendChild($classSynopsisInfo);
3758
3759            foreach ($this->constInfos as $constInfo) {
3760                $classSynopsis->appendChild(new DOMText("\n    "));
3761                $fieldSynopsisElement = $constInfo->getFieldSynopsisElement($doc, $allConstInfos);
3762                $classSynopsis->appendChild($fieldSynopsisElement);
3763            }
3764        }
3765
3766        if (!empty($this->propertyInfos)) {
3767            $classSynopsis->appendChild(new DOMText("\n\n    "));
3768            $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Properties;");
3769            $classSynopsisInfo->setAttribute("role", "comment");
3770            $classSynopsis->appendChild($classSynopsisInfo);
3771
3772            foreach ($this->propertyInfos as $propertyInfo) {
3773                $classSynopsis->appendChild(new DOMText("\n    "));
3774                $fieldSynopsisElement = $propertyInfo->getFieldSynopsisElement($doc, $allConstInfos);
3775                $classSynopsis->appendChild($fieldSynopsisElement);
3776            }
3777        }
3778
3779        $this->appendInheritedMemberSectionToClassSynopsis(
3780            $doc,
3781            $classSynopsis,
3782            $parentsWithInheritedProperties,
3783            "&Properties;",
3784            "&InheritedProperties;"
3785        );
3786
3787        if (!empty($this->funcInfos)) {
3788            $classSynopsis->appendChild(new DOMText("\n\n    "));
3789            $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&Methods;");
3790            $classSynopsisInfo->setAttribute("role", "comment");
3791            $classSynopsis->appendChild($classSynopsisInfo);
3792
3793            $classReference = self::getClassSynopsisReference($this->name);
3794            $escapedName = addslashes($this->name->__toString());
3795
3796            if ($this->hasConstructor()) {
3797                $classSynopsis->appendChild(new DOMText("\n    "));
3798                $includeElement = $this->createIncludeElement(
3799                    $doc,
3800                    "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:constructorsynopsis[@role='$escapedName'])"
3801                );
3802                $classSynopsis->appendChild($includeElement);
3803            }
3804
3805            if ($this->hasMethods()) {
3806                $classSynopsis->appendChild(new DOMText("\n    "));
3807                $includeElement = $this->createIncludeElement(
3808                    $doc,
3809                    "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:methodsynopsis[@role='$escapedName'])"
3810                );
3811                $classSynopsis->appendChild($includeElement);
3812            }
3813
3814            if ($this->hasDestructor()) {
3815                $classSynopsis->appendChild(new DOMText("\n    "));
3816                $includeElement = $this->createIncludeElement(
3817                    $doc,
3818                    "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$classReference')/db:refentry/db:refsect1[@role='description']/descendant::db:destructorsynopsis[@role='$escapedName'])"
3819                );
3820                $classSynopsis->appendChild($includeElement);
3821            }
3822        }
3823
3824        if (!empty($parentsWithInheritedMethods)) {
3825            $classSynopsis->appendChild(new DOMText("\n\n    "));
3826            $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "&InheritedMethods;");
3827            $classSynopsisInfo->setAttribute("role", "comment");
3828            $classSynopsis->appendChild($classSynopsisInfo);
3829
3830            foreach ($parentsWithInheritedMethods as $parent) {
3831                $parentName = $parent["name"];
3832                $parentMethodsynopsisTypes = $parent["types"];
3833
3834                $parentReference = self::getClassSynopsisReference($parentName);
3835                $escapedParentName = addslashes($parentName->__toString());
3836
3837                foreach ($parentMethodsynopsisTypes as $parentMethodsynopsisType) {
3838                    $classSynopsis->appendChild(new DOMText("\n    "));
3839                    $includeElement = $this->createIncludeElement(
3840                        $doc,
3841                        "xmlns(db=http://docbook.org/ns/docbook) xpointer(id('$parentReference')/db:refentry/db:refsect1[@role='description']/descendant::db:{$parentMethodsynopsisType}[@role='$escapedParentName'])"
3842                    );
3843
3844                    $classSynopsis->appendChild($includeElement);
3845                }
3846            }
3847        }
3848
3849        $classSynopsis->appendChild(new DOMText("\n   "));
3850
3851        return $classSynopsis;
3852    }
3853
3854    private static function createOoElement(
3855        DOMDocument $doc,
3856        ClassInfo $classInfo,
3857        ?string $typeOverride,
3858        bool $withModifiers,
3859        ?string $modifierOverride,
3860        int $indentationLevel
3861    ): ?DOMElement {
3862        $indentation = str_repeat(" ", $indentationLevel);
3863
3864        if ($classInfo->type !== "class" && $classInfo->type !== "interface") {
3865            echo "Class synopsis generation is not implemented for " . $classInfo->type . "\n";
3866            return null;
3867        }
3868
3869        $type = $typeOverride !== null ? $typeOverride : $classInfo->type;
3870
3871        $ooElement = $doc->createElement("oo$type");
3872        $ooElement->appendChild(new DOMText("\n$indentation "));
3873        if ($modifierOverride !== null) {
3874            $ooElement->appendChild($doc->createElement('modifier', $modifierOverride));
3875            $ooElement->appendChild(new DOMText("\n$indentation "));
3876        } elseif ($withModifiers) {
3877            foreach ($classInfo->attributes as $attribute) {
3878                $modifier = $doc->createElement("modifier", "#[\\" . $attribute->class . "]");
3879                $modifier->setAttribute("role", "attribute");
3880                $ooElement->appendChild($modifier);
3881                $ooElement->appendChild(new DOMText("\n$indentation "));
3882            }
3883
3884            if ($classInfo->flags & Modifiers::FINAL) {
3885                $ooElement->appendChild($doc->createElement('modifier', 'final'));
3886                $ooElement->appendChild(new DOMText("\n$indentation "));
3887            }
3888            if ($classInfo->flags & Modifiers::ABSTRACT) {
3889                $ooElement->appendChild($doc->createElement('modifier', 'abstract'));
3890                $ooElement->appendChild(new DOMText("\n$indentation "));
3891            }
3892            if ($classInfo->flags & Modifiers::READONLY) {
3893                $ooElement->appendChild($doc->createElement('modifier', 'readonly'));
3894                $ooElement->appendChild(new DOMText("\n$indentation "));
3895            }
3896        }
3897
3898        $nameElement = $doc->createElement("{$type}name", $classInfo->name->toString());
3899        $ooElement->appendChild($nameElement);
3900        $ooElement->appendChild(new DOMText("\n$indentation"));
3901
3902        return $ooElement;
3903    }
3904
3905    public static function getClassSynopsisFilename(Name $name): string {
3906        return strtolower(str_replace("_", "-", implode('-', $name->getParts())));
3907    }
3908
3909    public static function getClassSynopsisReference(Name $name): string {
3910        return "class." . self::getClassSynopsisFilename($name);
3911    }
3912
3913    /**
3914     * @param array<string, Name> $parentsWithInheritedConstants
3915     * @param array<string, Name> $parentsWithInheritedProperties
3916     * @param array<string, array{name: Name, types: int[]}> $parentsWithInheritedMethods
3917     * @param array<string, ClassInfo> $classMap
3918     */
3919    private function collectInheritedMembers(
3920        array &$parentsWithInheritedConstants,
3921        array &$parentsWithInheritedProperties,
3922        array &$parentsWithInheritedMethods,
3923        bool $hasConstructor,
3924        array $classMap
3925    ): void {
3926        foreach ($this->extends as $parent) {
3927            $parentInfo = $classMap[$parent->toString()] ?? null;
3928            $parentName = $parent->toString();
3929
3930            if (!$parentInfo) {
3931                throw new Exception("Missing parent class $parentName");
3932            }
3933
3934            if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parentName])) {
3935                $parentsWithInheritedConstants[] = $parent;
3936            }
3937
3938            if (!empty($parentInfo->propertyInfos) && !isset($parentsWithInheritedProperties[$parentName])) {
3939                $parentsWithInheritedProperties[$parentName] = $parent;
3940            }
3941
3942            if (!$hasConstructor && $parentInfo->hasNonPrivateConstructor()) {
3943                $parentsWithInheritedMethods[$parentName]["name"] = $parent;
3944                $parentsWithInheritedMethods[$parentName]["types"][] = "constructorsynopsis";
3945            }
3946
3947            if ($parentInfo->hasMethods()) {
3948                $parentsWithInheritedMethods[$parentName]["name"] = $parent;
3949                $parentsWithInheritedMethods[$parentName]["types"][] = "methodsynopsis";
3950            }
3951
3952            if ($parentInfo->hasDestructor()) {
3953                $parentsWithInheritedMethods[$parentName]["name"] = $parent;
3954                $parentsWithInheritedMethods[$parentName]["types"][] = "destructorsynopsis";
3955            }
3956
3957            $parentInfo->collectInheritedMembers(
3958                $parentsWithInheritedConstants,
3959                $parentsWithInheritedProperties,
3960                $parentsWithInheritedMethods,
3961                $hasConstructor,
3962                $classMap
3963            );
3964        }
3965
3966        foreach ($this->implements as $parent) {
3967            $parentInfo = $classMap[$parent->toString()] ?? null;
3968            if (!$parentInfo) {
3969                throw new Exception("Missing parent interface " . $parent->toString());
3970            }
3971
3972            if (!empty($parentInfo->constInfos) && !isset($parentsWithInheritedConstants[$parent->toString()])) {
3973                $parentsWithInheritedConstants[$parent->toString()] = $parent;
3974            }
3975
3976            $unusedParentsWithInheritedProperties = [];
3977            $unusedParentsWithInheritedMethods = [];
3978
3979            $parentInfo->collectInheritedMembers(
3980                $parentsWithInheritedConstants,
3981                $unusedParentsWithInheritedProperties,
3982                $unusedParentsWithInheritedMethods,
3983                $hasConstructor,
3984                $classMap
3985            );
3986        }
3987    }
3988
3989    /** @param array<string, ClassInfo> $classMap */
3990    private function isException(array $classMap): bool
3991    {
3992        if ($this->name->toString() === "Throwable") {
3993            return true;
3994        }
3995
3996        foreach ($this->extends as $parentName) {
3997            $parent = $classMap[$parentName->toString()] ?? null;
3998            if ($parent === null) {
3999                throw new Exception("Missing parent class " . $parentName->toString());
4000            }
4001
4002            if ($parent->isException($classMap)) {
4003                return true;
4004            }
4005        }
4006
4007        if ($this->type === "class") {
4008            foreach ($this->implements as $interfaceName) {
4009                $interface = $classMap[$interfaceName->toString()] ?? null;
4010                if ($interface === null) {
4011                    throw new Exception("Missing implemented interface " . $interfaceName->toString());
4012                }
4013
4014                if ($interface->isException($classMap)) {
4015                    return true;
4016                }
4017            }
4018        }
4019
4020        return false;
4021    }
4022
4023    private function hasConstructor(): bool
4024    {
4025        foreach ($this->funcInfos as $funcInfo) {
4026            if ($funcInfo->name->isConstructor()) {
4027                return true;
4028            }
4029        }
4030
4031        return false;
4032    }
4033
4034    private function hasNonPrivateConstructor(): bool
4035    {
4036        foreach ($this->funcInfos as $funcInfo) {
4037            if ($funcInfo->name->isConstructor() && !($funcInfo->flags & Modifiers::PRIVATE)) {
4038                return true;
4039            }
4040        }
4041
4042        return false;
4043    }
4044
4045    private function hasDestructor(): bool
4046    {
4047        foreach ($this->funcInfos as $funcInfo) {
4048            if ($funcInfo->name->isDestructor()) {
4049                return true;
4050            }
4051        }
4052
4053        return false;
4054    }
4055
4056    private function hasMethods(): bool
4057    {
4058        foreach ($this->funcInfos as $funcInfo) {
4059            if (!$funcInfo->name->isConstructor() && !$funcInfo->name->isDestructor()) {
4060                return true;
4061            }
4062        }
4063
4064        return false;
4065    }
4066
4067    private function createIncludeElement(DOMDocument $doc, string $query): DOMElement
4068    {
4069        $includeElement = $doc->createElement("xi:include");
4070        $attr = $doc->createAttribute("xpointer");
4071        $attr->value = $query;
4072        $includeElement->appendChild($attr);
4073        $fallbackElement = $doc->createElement("xi:fallback");
4074        $includeElement->appendChild(new DOMText("\n     "));
4075        $includeElement->appendChild($fallbackElement);
4076        $includeElement->appendChild(new DOMText("\n    "));
4077
4078        return $includeElement;
4079    }
4080
4081    public function __clone()
4082    {
4083        foreach ($this->constInfos as $key => $constInfo) {
4084            $this->constInfos[$key] = clone $constInfo;
4085        }
4086
4087        foreach ($this->propertyInfos as $key => $propertyInfo) {
4088            $this->propertyInfos[$key] = clone $propertyInfo;
4089        }
4090
4091        foreach ($this->funcInfos as $key => $funcInfo) {
4092            $this->funcInfos[$key] = clone $funcInfo;
4093        }
4094
4095        foreach ($this->attributes as $key => $attribute) {
4096            $this->attributes[$key] = clone $attribute;
4097        }
4098
4099        if ($this->exposedDocComment) {
4100            $this->exposedDocComment = clone $this->exposedDocComment;
4101        }
4102    }
4103
4104    /**
4105     * @param Name[] $parents
4106     */
4107    private function appendInheritedMemberSectionToClassSynopsis(DOMDocument $doc, DOMElement $classSynopsis, array $parents, string $label, string $inheritedLabel): void
4108    {
4109        if (empty($parents)) {
4110            return;
4111        }
4112
4113        $classSynopsis->appendChild(new DOMText("\n\n    "));
4114        $classSynopsisInfo = $doc->createElement("classsynopsisinfo", "$inheritedLabel");
4115        $classSynopsisInfo->setAttribute("role", "comment");
4116        $classSynopsis->appendChild($classSynopsisInfo);
4117
4118        foreach ($parents as $parent) {
4119            $classSynopsis->appendChild(new DOMText("\n    "));
4120            $parentReference = self::getClassSynopsisReference($parent);
4121
4122            $includeElement = $this->createIncludeElement(
4123                $doc,
4124                "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']]))"
4125            );
4126            $classSynopsis->appendChild($includeElement);
4127        }
4128    }
4129}
4130
4131class FileInfo {
4132    /** @var string[] */
4133    public array $dependencies = [];
4134    /** @var ConstInfo[] */
4135    public array $constInfos = [];
4136    /** @var FuncInfo[] */
4137    public array $funcInfos = [];
4138    /** @var ClassInfo[] */
4139    public array $classInfos = [];
4140    public bool $generateFunctionEntries = false;
4141    public string $declarationPrefix = "";
4142    public bool $generateClassEntries = false;
4143    public bool $isUndocumentable = false;
4144    public bool $legacyArginfoGeneration = false;
4145    private ?int $minimumPhpVersionIdCompatibility = null;
4146
4147    /**
4148     * @return iterable<FuncInfo>
4149     */
4150    public function getAllFuncInfos(): iterable {
4151        yield from $this->funcInfos;
4152        foreach ($this->classInfos as $classInfo) {
4153            yield from $classInfo->funcInfos;
4154        }
4155    }
4156
4157    /** @return array<string, ConstInfo> */
4158    public function getAllConstInfos(): array {
4159        $result = [];
4160
4161        foreach ($this->constInfos as $constInfo) {
4162            $result[$constInfo->name->__toString()] = $constInfo;
4163        }
4164
4165        foreach ($this->classInfos as $classInfo) {
4166            foreach ($classInfo->constInfos as $constInfo) {
4167                $result[$constInfo->name->__toString()] = $constInfo;
4168            }
4169        }
4170
4171        return $result;
4172    }
4173
4174    /**
4175     * @return iterable<ClassInfo>
4176     */
4177    public function getAllClassInfos(): iterable {
4178        foreach ($this->classInfos as $classInfo) {
4179            yield $classInfo;
4180        }
4181    }
4182
4183    public function __clone()
4184    {
4185        foreach ($this->constInfos as $key => $constInfo) {
4186            $this->constInfos[$key] = clone $constInfo;
4187        }
4188
4189        foreach ($this->funcInfos as $key => $funcInfo) {
4190            $this->funcInfos[$key] = clone $funcInfo;
4191        }
4192
4193        foreach ($this->classInfos as $key => $classInfo) {
4194            $this->classInfos[$key] = clone $classInfo;
4195        }
4196    }
4197
4198    public function setMinimumPhpVersionIdCompatibility(?int $minimumPhpVersionIdCompatibility) {
4199        $this->minimumPhpVersionIdCompatibility = $minimumPhpVersionIdCompatibility;
4200    }
4201
4202    public function getMinimumPhpVersionIdCompatibility(): ?int {
4203        // Non-legacy arginfo files are always PHP 8.0+ compatible
4204        if (!$this->legacyArginfoGeneration &&
4205            $this->minimumPhpVersionIdCompatibility !== null &&
4206            $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID
4207        ) {
4208            return PHP_80_VERSION_ID;
4209        }
4210
4211        return $this->minimumPhpVersionIdCompatibility;
4212    }
4213
4214    public function shouldGenerateLegacyArginfo(): bool {
4215        return $this->minimumPhpVersionIdCompatibility !== null && $this->minimumPhpVersionIdCompatibility < PHP_80_VERSION_ID;
4216    }
4217}
4218
4219class DocCommentTag {
4220    public string $name;
4221    public ?string $value;
4222
4223    public function __construct(string $name, ?string $value) {
4224        $this->name = $name;
4225        $this->value = $value;
4226    }
4227
4228    public function getValue(): string {
4229        if ($this->value === null) {
4230            throw new Exception("@$this->name does not have a value");
4231        }
4232
4233        return $this->value;
4234    }
4235
4236    public function getType(): string {
4237        $value = $this->getValue();
4238
4239        $matches = [];
4240
4241        if ($this->name === "param") {
4242            preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)\s*(?:[{(]|\$\w+).*$/', $value, $matches);
4243        } elseif ($this->name === "return" || $this->name === "var") {
4244            preg_match('/^\s*([\w\|\\\\\[\]<>, ]+)/', $value, $matches);
4245        }
4246
4247        if (!isset($matches[1])) {
4248            throw new Exception("@$this->name doesn't contain a type or has an invalid format \"$value\"");
4249        }
4250
4251        return trim($matches[1]);
4252    }
4253
4254    public function getVariableName(): string {
4255        $value = $this->value;
4256        if ($value === null || strlen($value) === 0) {
4257            throw new Exception("@$this->name doesn't have any value");
4258        }
4259
4260        $matches = [];
4261
4262        if ($this->name === "param") {
4263            // Allow for parsing extended types like callable(string):mixed in docblocks
4264            preg_match('/^\s*(?<type>[\w\|\\\\]+(?<parens>\((?<inparens>(?:(?&parens)|[^(){}[\]]*+))++\)|\{(?&inparens)\}|\[(?&inparens)\])*+(?::(?&type))?)\s*\$(?<name>\w+).*$/', $value, $matches);
4265        } elseif ($this->name === "prefer-ref") {
4266            preg_match('/^\s*\$(?<name>\w+).*$/', $value, $matches);
4267        }
4268
4269        if (!isset($matches["name"])) {
4270            throw new Exception("@$this->name doesn't contain a variable name or has an invalid format \"$value\"");
4271        }
4272
4273        return $matches["name"];
4274    }
4275}
4276
4277class ExposedDocComment {
4278    private string $docComment;
4279
4280    public function __construct(string $docComment) {
4281        $this->docComment = $docComment;
4282    }
4283
4284    public function escape(): string {
4285        return str_replace("\n", '\n', addslashes($this->docComment));
4286    }
4287
4288    public function getLength(): int {
4289        return strlen($this->docComment);
4290    }
4291}
4292
4293/** @return DocCommentTag[] */
4294function parseDocComments(array $comments): array {
4295    $tags = [];
4296    foreach ($comments as $comment) {
4297        if ($comment instanceof DocComment) {
4298            $tags = array_merge($tags, parseDocComment($comment));
4299        }
4300    }
4301
4302    return $tags;
4303}
4304
4305/** @return DocCommentTag[] */
4306function parseDocComment(DocComment $comment): array {
4307    $commentText = substr($comment->getText(), 2, -2);
4308    $tags = [];
4309    foreach (explode("\n", $commentText) as $commentLine) {
4310        $regex = '/^\*\s*@([a-z-]+)(?:\s+(.+))?$/';
4311        if (preg_match($regex, trim($commentLine), $matches)) {
4312            $tags[] = new DocCommentTag($matches[1], $matches[2] ?? null);
4313        }
4314    }
4315
4316    return $tags;
4317}
4318
4319class FramelessFunctionInfo {
4320    public int $arity;
4321}
4322
4323function parseFramelessFunctionInfo(string $json): FramelessFunctionInfo {
4324    // FIXME: Should have some validation
4325    $json = json_decode($json, true);
4326    $framelessFunctionInfo = new FramelessFunctionInfo();
4327    $framelessFunctionInfo->arity = $json["arity"];
4328    return $framelessFunctionInfo;
4329}
4330
4331function parseFunctionLike(
4332    PrettyPrinterAbstract $prettyPrinter,
4333    FunctionOrMethodName $name,
4334    int $classFlags,
4335    int $flags,
4336    Node\FunctionLike $func,
4337    ?string $cond,
4338    bool $isUndocumentable,
4339    ?int $minimumPhpVersionIdCompatibility
4340): FuncInfo {
4341    try {
4342        $comments = $func->getComments();
4343        $paramMeta = [];
4344        $aliasType = null;
4345        $alias = null;
4346        $isDeprecated = false;
4347        $supportsCompileTimeEval = false;
4348        $verify = true;
4349        $docReturnType = null;
4350        $tentativeReturnType = false;
4351        $docParamTypes = [];
4352        $refcount = null;
4353        $framelessFunctionInfos = [];
4354
4355        if ($comments) {
4356            $tags = parseDocComments($comments);
4357
4358            foreach ($tags as $tag) {
4359                switch ($tag->name) {
4360                    case 'alias':
4361                    case 'implementation-alias':
4362                        $aliasType = $tag->name;
4363                        $aliasParts = explode("::", $tag->getValue());
4364                        if (count($aliasParts) === 1) {
4365                            $alias = new FunctionName(new Name($aliasParts[0]));
4366                        } else {
4367                            $alias = new MethodName(new Name($aliasParts[0]), $aliasParts[1]);
4368                        }
4369                        break;
4370
4371                    case 'deprecated':
4372                        $isDeprecated = true;
4373                        break;
4374
4375                    case 'no-verify':
4376                        $verify = false;
4377                        break;
4378
4379                    case 'tentative-return-type':
4380                        $tentativeReturnType = true;
4381                        break;
4382
4383                    case 'return':
4384                        $docReturnType = $tag->getType();
4385                        break;
4386
4387                    case 'param':
4388                        $docParamTypes[$tag->getVariableName()] = $tag->getType();
4389                        break;
4390
4391                    case 'refcount':
4392                        $refcount = $tag->getValue();
4393                        break;
4394
4395                    case 'compile-time-eval':
4396                        $supportsCompileTimeEval = true;
4397                        break;
4398
4399                    case 'prefer-ref':
4400                        $varName = $tag->getVariableName();
4401                        if (!isset($paramMeta[$varName])) {
4402                            $paramMeta[$varName] = [];
4403                        }
4404                        $paramMeta[$varName][$tag->name] = true;
4405                        break;
4406
4407                    case 'undocumentable':
4408                        $isUndocumentable = true;
4409                        break;
4410
4411                    case 'frameless-function':
4412                        $framelessFunctionInfos[] = parseFramelessFunctionInfo($tag->getValue());
4413                        break;
4414                }
4415            }
4416        }
4417
4418        $varNameSet = [];
4419        $args = [];
4420        $numRequiredArgs = 0;
4421        $foundVariadic = false;
4422        foreach ($func->getParams() as $i => $param) {
4423            if ($param->isPromoted()) {
4424                throw new Exception("Promoted properties are not supported");
4425            }
4426
4427            $varName = $param->var->name;
4428            $preferRef = !empty($paramMeta[$varName]['prefer-ref']);
4429            unset($paramMeta[$varName]);
4430
4431            if (isset($varNameSet[$varName])) {
4432                throw new Exception("Duplicate parameter name $varName");
4433            }
4434            $varNameSet[$varName] = true;
4435
4436            if ($preferRef) {
4437                $sendBy = ArgInfo::SEND_PREFER_REF;
4438            } else if ($param->byRef) {
4439                $sendBy = ArgInfo::SEND_BY_REF;
4440            } else {
4441                $sendBy = ArgInfo::SEND_BY_VAL;
4442            }
4443
4444            if ($foundVariadic) {
4445                throw new Exception("Only the last parameter can be variadic");
4446            }
4447
4448            $type = $param->type ? Type::fromNode($param->type) : null;
4449            if ($type === null && !isset($docParamTypes[$varName])) {
4450                throw new Exception("Missing parameter type");
4451            }
4452
4453            if ($param->default instanceof Expr\ConstFetch &&
4454                $param->default->name->toLowerString() === "null" &&
4455                $type && !$type->isNullable()
4456            ) {
4457                $simpleType = $type->tryToSimpleType();
4458                if ($simpleType === null || !$simpleType->isMixed()) {
4459                    throw new Exception("Parameter $varName has null default, but is not nullable");
4460                }
4461            }
4462
4463            if ($param->default instanceof Expr\ClassConstFetch && $param->default->class->toLowerString() === "self") {
4464                throw new Exception('The exact class name must be used instead of "self"');
4465            }
4466
4467            $foundVariadic = $param->variadic;
4468
4469            $args[] = new ArgInfo(
4470                $varName,
4471                $sendBy,
4472                $param->variadic,
4473                $type,
4474                isset($docParamTypes[$varName]) ? Type::fromString($docParamTypes[$varName]) : null,
4475                $param->default ? $prettyPrinter->prettyPrintExpr($param->default) : null,
4476                createAttributes($param->attrGroups)
4477            );
4478            if (!$param->default && !$param->variadic) {
4479                $numRequiredArgs = $i + 1;
4480            }
4481        }
4482
4483        foreach (array_keys($paramMeta) as $var) {
4484            throw new Exception("Found metadata for invalid param $var");
4485        }
4486
4487        $returnType = $func->getReturnType();
4488        if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) {
4489            throw new Exception("Missing return type");
4490        }
4491
4492        $return = new ReturnInfo(
4493            $func->returnsByRef(),
4494            $returnType ? Type::fromNode($returnType) : null,
4495            $docReturnType ? Type::fromString($docReturnType) : null,
4496            $tentativeReturnType,
4497            $refcount
4498        );
4499
4500        return new FuncInfo(
4501            $name,
4502            $classFlags,
4503            $flags,
4504            $aliasType,
4505            $alias,
4506            $isDeprecated,
4507            $supportsCompileTimeEval,
4508            $verify,
4509            $args,
4510            $return,
4511            $numRequiredArgs,
4512            $cond,
4513            $isUndocumentable,
4514            $minimumPhpVersionIdCompatibility,
4515            createAttributes($func->attrGroups),
4516            $framelessFunctionInfos,
4517            createExposedDocComment($comments)
4518        );
4519    } catch (Exception $e) {
4520        throw new Exception($name . "(): " .$e->getMessage());
4521    }
4522}
4523
4524/**
4525 * @param array<int, array<int, AttributeGroup> $attributes
4526 */
4527function parseConstLike(
4528    PrettyPrinterAbstract $prettyPrinter,
4529    ConstOrClassConstName $name,
4530    Node\Const_ $const,
4531    int $flags,
4532    ?Node $type,
4533    array $comments,
4534    ?string $cond,
4535    bool $isUndocumentable,
4536    ?int $phpVersionIdMinimumCompatibility,
4537    array $attributes
4538): ConstInfo {
4539    $phpDocType = null;
4540    $deprecated = false;
4541    $cValue = null;
4542    $link = null;
4543    $isFileCacheAllowed = true;
4544    if ($comments) {
4545        $tags = parseDocComments($comments);
4546        foreach ($tags as $tag) {
4547            if ($tag->name === 'var') {
4548                $phpDocType = $tag->getType();
4549            } elseif ($tag->name === 'deprecated') {
4550                $deprecated = true;
4551            } elseif ($tag->name === 'cvalue') {
4552                $cValue = $tag->value;
4553            } elseif ($tag->name === 'undocumentable') {
4554                $isUndocumentable = true;
4555            } elseif ($tag->name === 'link') {
4556                $link = $tag->value;
4557            } elseif ($tag->name === 'no-file-cache') {
4558                $isFileCacheAllowed = false;
4559            }
4560        }
4561    }
4562
4563    if ($type === null && $phpDocType === null) {
4564        throw new Exception("Missing type for constant " . $name->__toString());
4565    }
4566
4567    $constType = $type ? Type::fromNode($type) : null;
4568    $constPhpDocType = $phpDocType ? Type::fromString($phpDocType) : null;
4569
4570    if ($const->value instanceof Expr\ConstFetch &&
4571        $const->value->name->toLowerString() === "null" &&
4572        $constType && !$constType->isNullable()
4573    ) {
4574        $simpleType = $constType->tryToSimpleType();
4575        if ($simpleType === null || !$simpleType->isMixed()) {
4576            throw new Exception("Constant " . $name->__toString() . " has null value, but is not nullable");
4577        }
4578    }
4579
4580    return new ConstInfo(
4581        $name,
4582        $flags,
4583        $const->value,
4584        $prettyPrinter->prettyPrintExpr($const->value),
4585        $constType,
4586        $constPhpDocType,
4587        $deprecated,
4588        $cond,
4589        $cValue,
4590        $isUndocumentable,
4591        $link,
4592        $phpVersionIdMinimumCompatibility,
4593        $attributes,
4594        createExposedDocComment($comments),
4595        $isFileCacheAllowed
4596    );
4597}
4598
4599/**
4600 * @param array<int, array<int, AttributeGroup> $attributes
4601 */
4602function parseProperty(
4603    Name $class,
4604    int $classFlags,
4605    int $flags,
4606    Stmt\PropertyProperty $property,
4607    ?Node $type,
4608    array $comments,
4609    PrettyPrinterAbstract $prettyPrinter,
4610    ?int $phpVersionIdMinimumCompatibility,
4611    array $attributes
4612): PropertyInfo {
4613    $phpDocType = null;
4614    $isDocReadonly = false;
4615    $isVirtual = false;
4616    $link = null;
4617
4618    if ($comments) {
4619        $tags = parseDocComments($comments);
4620        foreach ($tags as $tag) {
4621            if ($tag->name === 'var') {
4622                $phpDocType = $tag->getType();
4623            } elseif ($tag->name === 'readonly') {
4624                $isDocReadonly = true;
4625            } elseif ($tag->name === 'link') {
4626                $link = $tag->value;
4627            } elseif ($tag->name === 'virtual') {
4628                $isVirtual = true;
4629            }
4630        }
4631    }
4632
4633    $propertyType = $type ? Type::fromNode($type) : null;
4634    if ($propertyType === null && !$phpDocType) {
4635        throw new Exception("Missing type for property $class::\$$property->name");
4636    }
4637
4638    if ($property->default instanceof Expr\ConstFetch &&
4639        $property->default->name->toLowerString() === "null" &&
4640        $propertyType && !$propertyType->isNullable()
4641    ) {
4642        $simpleType = $propertyType->tryToSimpleType();
4643        if ($simpleType === null || !$simpleType->isMixed()) {
4644            throw new Exception("Property $class::\$$property->name has null default, but is not nullable");
4645        }
4646    }
4647
4648    return new PropertyInfo(
4649        new PropertyName($class, $property->name->__toString()),
4650        $classFlags,
4651        $flags,
4652        $propertyType,
4653        $phpDocType ? Type::fromString($phpDocType) : null,
4654        $property->default,
4655        $property->default ? $prettyPrinter->prettyPrintExpr($property->default) : null,
4656        $isDocReadonly,
4657        $isVirtual,
4658        $link,
4659        $phpVersionIdMinimumCompatibility,
4660        $attributes,
4661        createExposedDocComment($comments)
4662    );
4663}
4664
4665/**
4666 * @param ConstInfo[] $consts
4667 * @param PropertyInfo[] $properties
4668 * @param FuncInfo[] $methods
4669 * @param EnumCaseInfo[] $enumCases
4670 */
4671function parseClass(
4672    Name $name,
4673    Stmt\ClassLike $class,
4674    array $consts,
4675    array $properties,
4676    array $methods,
4677    array $enumCases,
4678    ?string $cond,
4679    ?int $minimumPhpVersionIdCompatibility,
4680    bool $isUndocumentable
4681): ClassInfo {
4682    $flags = $class instanceof Class_ ? $class->flags : 0;
4683    $comments = $class->getComments();
4684    $alias = null;
4685    $isDeprecated = false;
4686    $isStrictProperties = false;
4687    $isNotSerializable = false;
4688    $allowsDynamicProperties = false;
4689    $attributes = [];
4690
4691    if ($comments) {
4692        $tags = parseDocComments($comments);
4693        foreach ($tags as $tag) {
4694            if ($tag->name === 'alias') {
4695                $alias = $tag->getValue();
4696            } else if ($tag->name === 'deprecated') {
4697                $isDeprecated = true;
4698            } else if ($tag->name === 'strict-properties') {
4699                $isStrictProperties = true;
4700            } else if ($tag->name === 'not-serializable') {
4701                $isNotSerializable = true;
4702            } else if ($tag->name === 'undocumentable') {
4703                $isUndocumentable = true;
4704            }
4705        }
4706    }
4707
4708    $attributes = createAttributes($class->attrGroups);
4709    foreach ($attributes as $attribute) {
4710        switch ($attribute->class) {
4711            case 'AllowDynamicProperties':
4712                $allowsDynamicProperties = true;
4713                break 2;
4714        }
4715    }
4716
4717    if ($isStrictProperties && $allowsDynamicProperties) {
4718        throw new Exception("A class may not have '@strict-properties' and '#[\\AllowDynamicProperties]' at the same time.");
4719    }
4720
4721    $extends = [];
4722    $implements = [];
4723
4724    if ($class instanceof Class_) {
4725        $classKind = "class";
4726        if ($class->extends) {
4727            $extends[] = $class->extends;
4728        }
4729        $implements = $class->implements;
4730    } elseif ($class instanceof Interface_) {
4731        $classKind = "interface";
4732        $extends = $class->extends;
4733    } else if ($class instanceof Trait_) {
4734        $classKind = "trait";
4735    } else if ($class instanceof Enum_) {
4736        $classKind = "enum";
4737        $implements = $class->implements;
4738    } else {
4739        throw new Exception("Unknown class kind " . get_class($class));
4740    }
4741
4742    if ($isUndocumentable) {
4743        foreach ($methods as $method) {
4744            $method->isUndocumentable = true;
4745        }
4746    }
4747
4748    return new ClassInfo(
4749        $name,
4750        $flags,
4751        $classKind,
4752        $alias,
4753        $class instanceof Enum_ && $class->scalarType !== null
4754            ? SimpleType::fromNode($class->scalarType) : null,
4755        $isDeprecated,
4756        $isStrictProperties,
4757        $attributes,
4758        createExposedDocComment($comments),
4759        $isNotSerializable,
4760        $extends,
4761        $implements,
4762        $consts,
4763        $properties,
4764        $methods,
4765        $enumCases,
4766        $cond,
4767        $minimumPhpVersionIdCompatibility,
4768        $isUndocumentable
4769    );
4770}
4771
4772/**
4773 * @param array<int, array<int, AttributeGroup>> $attributeGroups
4774 * @return Attribute[]
4775 */
4776function createAttributes(array $attributeGroups): array {
4777    $attributes = [];
4778
4779    foreach ($attributeGroups as $attrGroup) {
4780        foreach ($attrGroup->attrs as $attr) {
4781            $attributes[] = new AttributeInfo($attr->name->toString(), $attr->args);
4782        }
4783    }
4784
4785    return $attributes;
4786}
4787
4788/** @param array<int, DocComment> $comments */
4789function createExposedDocComment(array $comments): ?ExposedDocComment {
4790    $exposedDocComment = null;
4791
4792    foreach ($comments as $comment) {
4793        $text = $comment->getText();
4794        $matches = [];
4795        $pattern = "#^(\s*\/\*\*)(\s*@genstubs-expose-comment-block)(\s*)$#m";
4796
4797        if (preg_match($pattern, $text, $matches) !== 1) {
4798            continue;
4799        }
4800
4801        if ($exposedDocComment !== null) {
4802            throw new Exception("Only one PHPDoc comment block can be exposed");
4803        }
4804
4805        $exposedDocComment = preg_replace($pattern, '$1$3', $text);
4806    }
4807
4808    return $exposedDocComment ? new ExposedDocComment($exposedDocComment) : null;
4809}
4810
4811function handlePreprocessorConditions(array &$conds, Stmt $stmt): ?string {
4812    foreach ($stmt->getComments() as $comment) {
4813        $text = trim($comment->getText());
4814        if (preg_match('/^#\s*if\s+(.+)$/', $text, $matches)) {
4815            $conds[] = $matches[1];
4816        } else if (preg_match('/^#\s*ifdef\s+(.+)$/', $text, $matches)) {
4817            $conds[] = "defined($matches[1])";
4818        } else if (preg_match('/^#\s*ifndef\s+(.+)$/', $text, $matches)) {
4819            $conds[] = "!defined($matches[1])";
4820        } else if (preg_match('/^#\s*else$/', $text)) {
4821            if (empty($conds)) {
4822                throw new Exception("Encountered else without corresponding #if");
4823            }
4824            $cond = array_pop($conds);
4825            $conds[] = "!($cond)";
4826        } else if (preg_match('/^#\s*endif$/', $text)) {
4827            if (empty($conds)) {
4828                throw new Exception("Encountered #endif without corresponding #if");
4829            }
4830            array_pop($conds);
4831        } else if ($text[0] === '#') {
4832            throw new Exception("Unrecognized preprocessor directive \"$text\"");
4833        }
4834    }
4835
4836    return empty($conds) ? null : implode(' && ', $conds);
4837}
4838
4839/** @return DocComment[] */
4840function getFileDocComments(array $stmts): array {
4841    if (empty($stmts)) {
4842        return [];
4843    }
4844
4845    $comments = $stmts[0]->getComments();
4846
4847    $result = [];
4848    foreach ($comments as $comment) {
4849        if ($comment instanceof DocComment) {
4850            $result[] = $comment;
4851        }
4852    }
4853
4854    return $result;
4855}
4856
4857function handleStatements(FileInfo $fileInfo, array $stmts, PrettyPrinterAbstract $prettyPrinter) {
4858    $conds = [];
4859    foreach ($stmts as $stmt) {
4860        $cond = handlePreprocessorConditions($conds, $stmt);
4861
4862        if ($stmt instanceof Stmt\Nop) {
4863            continue;
4864        }
4865
4866        if ($stmt instanceof Stmt\Namespace_) {
4867            handleStatements($fileInfo, $stmt->stmts, $prettyPrinter);
4868            continue;
4869        }
4870
4871        if ($stmt instanceof Stmt\Const_) {
4872            foreach ($stmt->consts as $const) {
4873                $fileInfo->constInfos[] = parseConstLike(
4874                    $prettyPrinter,
4875                    new ConstName($const->namespacedName, $const->name->toString()),
4876                    $const,
4877                    0,
4878                    null,
4879                    $stmt->getComments(),
4880                    $cond,
4881                    $fileInfo->isUndocumentable,
4882                    $fileInfo->getMinimumPhpVersionIdCompatibility(),
4883                    []
4884                );
4885            }
4886            continue;
4887        }
4888
4889        if ($stmt instanceof Stmt\Function_) {
4890            $fileInfo->funcInfos[] = parseFunctionLike(
4891                $prettyPrinter,
4892                new FunctionName($stmt->namespacedName),
4893                0,
4894                0,
4895                $stmt,
4896                $cond,
4897                $fileInfo->isUndocumentable,
4898                $fileInfo->getMinimumPhpVersionIdCompatibility()
4899            );
4900            continue;
4901        }
4902
4903        if ($stmt instanceof Stmt\ClassLike) {
4904            $className = $stmt->namespacedName;
4905            $constInfos = [];
4906            $propertyInfos = [];
4907            $methodInfos = [];
4908            $enumCaseInfos = [];
4909            foreach ($stmt->stmts as $classStmt) {
4910                $cond = handlePreprocessorConditions($conds, $classStmt);
4911                if ($classStmt instanceof Stmt\Nop) {
4912                    continue;
4913                }
4914
4915                $classFlags = $stmt instanceof Class_ ? $stmt->flags : 0;
4916                $abstractFlag = $stmt instanceof Stmt\Interface_ ? Modifiers::ABSTRACT : 0;
4917
4918                if ($classStmt instanceof Stmt\ClassConst) {
4919                    foreach ($classStmt->consts as $const) {
4920                        $constInfos[] = parseConstLike(
4921                            $prettyPrinter,
4922                            new ClassConstName($className, $const->name->toString()),
4923                            $const,
4924                            $classStmt->flags,
4925                            $classStmt->type,
4926                            $classStmt->getComments(),
4927                            $cond,
4928                            $fileInfo->isUndocumentable,
4929                            $fileInfo->getMinimumPhpVersionIdCompatibility(),
4930                            createAttributes($classStmt->attrGroups)
4931                        );
4932                    }
4933                } else if ($classStmt instanceof Stmt\Property) {
4934                    if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) {
4935                        throw new Exception("Visibility modifier is required");
4936                    }
4937                    foreach ($classStmt->props as $property) {
4938                        $propertyInfos[] = parseProperty(
4939                            $className,
4940                            $classFlags,
4941                            $classStmt->flags,
4942                            $property,
4943                            $classStmt->type,
4944                            $classStmt->getComments(),
4945                            $prettyPrinter,
4946                            $fileInfo->getMinimumPhpVersionIdCompatibility(),
4947                            createAttributes($classStmt->attrGroups)
4948                        );
4949                    }
4950                } else if ($classStmt instanceof Stmt\ClassMethod) {
4951                    if (!($classStmt->flags & Class_::VISIBILITY_MODIFIER_MASK)) {
4952                        throw new Exception("Visibility modifier is required");
4953                    }
4954                    $methodInfos[] = parseFunctionLike(
4955                        $prettyPrinter,
4956                        new MethodName($className, $classStmt->name->toString()),
4957                        $classFlags,
4958                        $classStmt->flags | $abstractFlag,
4959                        $classStmt,
4960                        $cond,
4961                        $fileInfo->isUndocumentable,
4962                        $fileInfo->getMinimumPhpVersionIdCompatibility()
4963                    );
4964                } else if ($classStmt instanceof Stmt\EnumCase) {
4965                    $enumCaseInfos[] = new EnumCaseInfo(
4966                        $classStmt->name->toString(), $classStmt->expr);
4967                } else {
4968                    throw new Exception("Not implemented {$classStmt->getType()}");
4969                }
4970            }
4971
4972            $fileInfo->classInfos[] = parseClass(
4973                $className, $stmt, $constInfos, $propertyInfos, $methodInfos, $enumCaseInfos, $cond, $fileInfo->getMinimumPhpVersionIdCompatibility(), $fileInfo->isUndocumentable
4974            );
4975            continue;
4976        }
4977
4978        if ($stmt instanceof Stmt\Expression) {
4979            $expr = $stmt->expr;
4980            if ($expr instanceof Expr\Include_) {
4981                $fileInfo->dependencies[] = (string)EvaluatedValue::createFromExpression($expr->expr, null, null, [])->value;
4982                continue;
4983            }
4984        }
4985
4986        throw new Exception("Unexpected node {$stmt->getType()}");
4987    }
4988    if (!empty($conds)) {
4989        throw new Exception("Unterminated preprocessor conditions");
4990    }
4991}
4992
4993function parseStubFile(string $code): FileInfo {
4994    $lexer = new PhpParser\Lexer\Emulative();
4995    $parser = new PhpParser\Parser\Php7($lexer);
4996    $nodeTraverser = new PhpParser\NodeTraverser;
4997    $nodeTraverser->addVisitor(new PhpParser\NodeVisitor\NameResolver);
4998    $prettyPrinter = new class extends Standard {
4999        protected function pName_FullyQualified(Name\FullyQualified $node): string {
5000            return implode('\\', $node->getParts());
5001        }
5002    };
5003
5004    $stmts = $parser->parse($code);
5005    $nodeTraverser->traverse($stmts);
5006
5007    $fileInfo = new FileInfo;
5008    $fileDocComments = getFileDocComments($stmts);
5009    if ($fileDocComments !== []) {
5010        $fileTags = parseDocComments($fileDocComments);
5011        foreach ($fileTags as $tag) {
5012            if ($tag->name === 'generate-function-entries') {
5013                $fileInfo->generateFunctionEntries = true;
5014                $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : "";
5015            } else if ($tag->name === 'generate-legacy-arginfo') {
5016                if ($tag->value && !in_array((int) $tag->value, ALL_PHP_VERSION_IDS, true)) {
5017                    throw new Exception(
5018                        "Legacy PHP version must be one of: \"" . PHP_70_VERSION_ID . "\" (PHP 7.0), \"" . PHP_80_VERSION_ID . "\" (PHP 8.0), " .
5019                        "\"" . PHP_81_VERSION_ID . "\" (PHP 8.1), \"" . PHP_82_VERSION_ID . "\" (PHP 8.2), \"" . PHP_83_VERSION_ID . "\" (PHP 8.3), " .
5020                        "\"" . PHP_84_VERSION_ID . "\" (PHP 8.4), \"" . $tag->value . "\" provided"
5021                    );
5022                }
5023
5024                $fileInfo->setMinimumPhpVersionIdCompatibility($tag->value ? (int) $tag->value : PHP_70_VERSION_ID);
5025            } else if ($tag->name === 'generate-class-entries') {
5026                $fileInfo->generateClassEntries = true;
5027                $fileInfo->declarationPrefix = $tag->value ? $tag->value . " " : "";
5028            } else if ($tag->name === 'undocumentable') {
5029                $fileInfo->isUndocumentable = true;
5030            }
5031        }
5032    }
5033
5034    // Generating class entries require generating function/method entries
5035    if ($fileInfo->generateClassEntries && !$fileInfo->generateFunctionEntries) {
5036        $fileInfo->generateFunctionEntries = true;
5037    }
5038
5039    handleStatements($fileInfo, $stmts, $prettyPrinter);
5040    return $fileInfo;
5041}
5042
5043function funcInfoToCode(FileInfo $fileInfo, FuncInfo $funcInfo): string {
5044    $code = '';
5045    $returnType = $funcInfo->return->type;
5046    $isTentativeReturnType = $funcInfo->return->tentativeReturnType;
5047    $php81MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_81_VERSION_ID;
5048
5049    if ($returnType !== null) {
5050        if ($isTentativeReturnType && !$php81MinimumCompatibility) {
5051            $code .= "#if (PHP_VERSION_ID >= " . PHP_81_VERSION_ID . ")\n";
5052        }
5053        if (null !== $simpleReturnType = $returnType->tryToSimpleType()) {
5054            if ($simpleReturnType->isBuiltin) {
5055                $code .= sprintf(
5056                    "%s(%s, %d, %d, %s, %d)\n",
5057                    $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX",
5058                    $funcInfo->getArgInfoName(), $funcInfo->return->byRef,
5059                    $funcInfo->numRequiredArgs,
5060                    $simpleReturnType->toTypeCode(), $returnType->isNullable()
5061                );
5062            } else {
5063                $code .= sprintf(
5064                    "%s(%s, %d, %d, %s, %d)\n",
5065                    $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX",
5066                    $funcInfo->getArgInfoName(), $funcInfo->return->byRef,
5067                    $funcInfo->numRequiredArgs,
5068                    $simpleReturnType->toEscapedName(), $returnType->isNullable()
5069                );
5070            }
5071        } else {
5072            $arginfoType = $returnType->toArginfoType();
5073            if ($arginfoType->hasClassType()) {
5074                $code .= sprintf(
5075                    "%s(%s, %d, %d, %s, %s)\n",
5076                    $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX",
5077                    $funcInfo->getArgInfoName(), $funcInfo->return->byRef,
5078                    $funcInfo->numRequiredArgs,
5079                    $arginfoType->toClassTypeString(), $arginfoType->toTypeMask()
5080                );
5081            } else {
5082                $code .= sprintf(
5083                    "%s(%s, %d, %d, %s)\n",
5084                    $isTentativeReturnType ? "ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX" : "ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX",
5085                    $funcInfo->getArgInfoName(), $funcInfo->return->byRef,
5086                    $funcInfo->numRequiredArgs,
5087                    $arginfoType->toTypeMask()
5088                );
5089            }
5090        }
5091        if ($isTentativeReturnType && !$php81MinimumCompatibility) {
5092            $code .= sprintf(
5093                "#else\nZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n#endif\n",
5094                $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs
5095            );
5096        }
5097    } else {
5098        $code .= sprintf(
5099            "ZEND_BEGIN_ARG_INFO_EX(%s, 0, %d, %d)\n",
5100            $funcInfo->getArgInfoName(), $funcInfo->return->byRef, $funcInfo->numRequiredArgs
5101        );
5102    }
5103
5104    foreach ($funcInfo->args as $argInfo) {
5105        $argKind = $argInfo->isVariadic ? "ARG_VARIADIC" : "ARG";
5106        $argDefaultKind = $argInfo->hasProperDefaultValue() ? "_WITH_DEFAULT_VALUE" : "";
5107        $argType = $argInfo->type;
5108        if ($argType !== null) {
5109            if (null !== $simpleArgType = $argType->tryToSimpleType()) {
5110                if ($simpleArgType->isBuiltin) {
5111                    $code .= sprintf(
5112                        "\tZEND_%s_TYPE_INFO%s(%s, %s, %s, %d%s)\n",
5113                        $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name,
5114                        $simpleArgType->toTypeCode(), $argType->isNullable(),
5115                        $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
5116                    );
5117                } else {
5118                    $code .= sprintf(
5119                        "\tZEND_%s_OBJ_INFO%s(%s, %s, %s, %d%s)\n",
5120                        $argKind,$argDefaultKind, $argInfo->getSendByString(), $argInfo->name,
5121                        $simpleArgType->toEscapedName(), $argType->isNullable(),
5122                        $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
5123                    );
5124                }
5125            } else {
5126                $arginfoType = $argType->toArginfoType();
5127                if ($arginfoType->hasClassType()) {
5128                    $code .= sprintf(
5129                        "\tZEND_%s_OBJ_TYPE_MASK(%s, %s, %s, %s%s)\n",
5130                        $argKind, $argInfo->getSendByString(), $argInfo->name,
5131                        $arginfoType->toClassTypeString(), $arginfoType->toTypeMask(),
5132                        !$argInfo->isVariadic ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
5133                    );
5134                } else {
5135                    $code .= sprintf(
5136                        "\tZEND_%s_TYPE_MASK(%s, %s, %s, %s)\n",
5137                        $argKind, $argInfo->getSendByString(), $argInfo->name,
5138                        $arginfoType->toTypeMask(),
5139                        $argInfo->getDefaultValueAsArginfoString()
5140                    );
5141                }
5142            }
5143        } else {
5144            $code .= sprintf(
5145                "\tZEND_%s_INFO%s(%s, %s%s)\n",
5146                $argKind, $argDefaultKind, $argInfo->getSendByString(), $argInfo->name,
5147                $argInfo->hasProperDefaultValue() ? ", " . $argInfo->getDefaultValueAsArginfoString() : ""
5148            );
5149        }
5150    }
5151
5152    $code .= "ZEND_END_ARG_INFO()";
5153    return $code . "\n";
5154}
5155
5156/** @param FuncInfo[] $generatedFuncInfos */
5157function findEquivalentFuncInfo(array $generatedFuncInfos, FuncInfo $funcInfo): ?FuncInfo {
5158    foreach ($generatedFuncInfos as $generatedFuncInfo) {
5159        if ($generatedFuncInfo->equalsApartFromNameAndRefcount($funcInfo)) {
5160            return $generatedFuncInfo;
5161        }
5162    }
5163    return null;
5164}
5165
5166/**
5167 * @template T
5168 * @param iterable<T> $infos
5169 * @param Closure(T): string|null $codeGenerator
5170 * @param ?string $parentCond
5171 */
5172function generateCodeWithConditions(
5173    iterable $infos, string $separator, Closure $codeGenerator, ?string $parentCond = null): string {
5174    $code = "";
5175
5176    // For combining the conditional blocks of the infos with the same condition
5177    $openCondition = null;
5178    foreach ($infos as $info) {
5179        $infoCode = $codeGenerator($info);
5180        if ($infoCode === null) {
5181            continue;
5182        }
5183
5184        if ($info->cond && $info->cond !== $parentCond) {
5185            if ($openCondition !== null
5186                && $info->cond !== $openCondition
5187            ) {
5188                // Changing condition, end old
5189                $code .= "#endif\n";
5190                $code .= $separator;
5191                $code .= "#if {$info->cond}\n";
5192                $openCondition = $info->cond;
5193            } elseif ($openCondition === null) {
5194                // New condition with no existing one
5195                $code .= $separator;
5196                $code .= "#if {$info->cond}\n";
5197                $openCondition = $info->cond;
5198            } else {
5199                // Staying in the same condition
5200                $code .= $separator;
5201            }
5202            $code .= $infoCode;
5203        } else {
5204            if ($openCondition !== null) {
5205                // Ending the condition
5206                $code .= "#endif\n";
5207                $openCondition = null;
5208            }
5209            $code .= $separator;
5210            $code .= $infoCode;
5211        }
5212    }
5213    // The last info might have been in a conditional block
5214    if ($openCondition !== null) {
5215        $code .= "#endif\n";
5216    }
5217
5218    return $code;
5219}
5220
5221/**
5222 * @param array<string, ConstInfo> $allConstInfos
5223 */
5224function generateArgInfoCode(
5225    string $stubFilenameWithoutExtension,
5226    FileInfo $fileInfo,
5227    array $allConstInfos,
5228    string $stubHash
5229): string {
5230    $code = "/* This is a generated file, edit the .stub.php file instead.\n"
5231          . " * Stub hash: $stubHash */\n";
5232
5233    $generatedFuncInfos = [];
5234
5235    $argInfoCode = generateCodeWithConditions(
5236        $fileInfo->getAllFuncInfos(), "\n",
5237        static function (FuncInfo $funcInfo) use (&$generatedFuncInfos, $fileInfo) {
5238            /* If there already is an equivalent arginfo structure, only emit a #define */
5239            if ($generatedFuncInfo = findEquivalentFuncInfo($generatedFuncInfos, $funcInfo)) {
5240                $code = sprintf(
5241                    "#define %s %s\n",
5242                    $funcInfo->getArgInfoName(), $generatedFuncInfo->getArgInfoName()
5243                );
5244            } else {
5245                $code = funcInfoToCode($fileInfo, $funcInfo);
5246            }
5247
5248            $generatedFuncInfos[] = $funcInfo;
5249            return $code;
5250        }
5251    );
5252
5253    if ($argInfoCode !== "") {
5254        $code .= "$argInfoCode\n";
5255    }
5256
5257    if ($fileInfo->generateFunctionEntries) {
5258        $framelessFunctionCode = generateCodeWithConditions(
5259            $fileInfo->getAllFuncInfos(), "\n",
5260            static function (FuncInfo $funcInfo) {
5261                $code = $funcInfo->getFramelessDeclaration($funcInfo);
5262                return $code;
5263            }
5264        );
5265
5266        if ($framelessFunctionCode !== "") {
5267            $code .= "$framelessFunctionCode\n";
5268        }
5269
5270        $generatedFunctionDeclarations = [];
5271        $code .= generateCodeWithConditions(
5272            $fileInfo->getAllFuncInfos(), "",
5273            static function (FuncInfo $funcInfo) use ($fileInfo, &$generatedFunctionDeclarations) {
5274                $key = $funcInfo->getDeclarationKey();
5275                if (isset($generatedFunctionDeclarations[$key])) {
5276                    return null;
5277                }
5278
5279                $generatedFunctionDeclarations[$key] = true;
5280                return $fileInfo->declarationPrefix . $funcInfo->getDeclaration();
5281            }
5282        );
5283
5284        $code .= generateFunctionEntries(null, $fileInfo->funcInfos);
5285
5286        foreach ($fileInfo->classInfos as $classInfo) {
5287            $code .= generateFunctionEntries($classInfo->name, $classInfo->funcInfos, $classInfo->cond);
5288        }
5289    }
5290
5291    $php80MinimumCompatibility = $fileInfo->getMinimumPhpVersionIdCompatibility() === null || $fileInfo->getMinimumPhpVersionIdCompatibility() >= PHP_80_VERSION_ID;
5292
5293    if ($fileInfo->generateClassEntries) {
5294        if ($attributeInitializationCode = generateFunctionAttributeInitialization($fileInfo->funcInfos, $allConstInfos, $fileInfo->getMinimumPhpVersionIdCompatibility(), null)) {
5295            if (!$php80MinimumCompatibility) {
5296                $attributeInitializationCode = "\n#if (PHP_VERSION_ID >= " . PHP_80_VERSION_ID . ")" . $attributeInitializationCode . "#endif\n";
5297            }
5298        }
5299
5300        if ($attributeInitializationCode !== "" || !empty($fileInfo->constInfos)) {
5301            $code .= "\nstatic void register_{$stubFilenameWithoutExtension}_symbols(int module_number)\n";
5302            $code .= "{\n";
5303
5304            foreach ($fileInfo->constInfos as $constInfo) {
5305                $code .= $constInfo->getDeclaration($allConstInfos);
5306            }
5307
5308            if ($attributeInitializationCode !== "" && $fileInfo->constInfos) {
5309                $code .= "\n";
5310            }
5311
5312            $code .= $attributeInitializationCode;
5313            $code .= "}\n";
5314        }
5315
5316        $code .= generateClassEntryCode($fileInfo, $allConstInfos);
5317    }
5318
5319    return $code;
5320}
5321
5322/** @param array<string, ConstInfo> $allConstInfos */
5323function generateClassEntryCode(FileInfo $fileInfo, array $allConstInfos): string {
5324    $code = "";
5325
5326    foreach ($fileInfo->classInfos as $class) {
5327        $code .= "\n" . $class->getRegistration($allConstInfos);
5328    }
5329
5330    return $code;
5331}
5332
5333/** @param FuncInfo[] $funcInfos */
5334function generateFunctionEntries(?Name $className, array $funcInfos, ?string $cond = null): string {
5335    // No need to add anything if there are no function entries
5336    if ($funcInfos === []) {
5337        return '';
5338    }
5339
5340    $code = "\n";
5341
5342    if ($cond) {
5343        $code .= "#if {$cond}\n";
5344    }
5345
5346    $functionEntryName = "ext_functions";
5347    if ($className) {
5348        $underscoreName = implode("_", $className->getParts());
5349        $functionEntryName = "class_{$underscoreName}_methods";
5350    }
5351
5352    $code .= "static const zend_function_entry {$functionEntryName}[] = {\n";
5353    $code .= generateCodeWithConditions($funcInfos, "", static function (FuncInfo $funcInfo) {
5354        return $funcInfo->getFunctionEntry();
5355    }, $cond);
5356    $code .= "\tZEND_FE_END\n";
5357    $code .= "};\n";
5358
5359    if ($cond) {
5360        $code .= "#endif\n";
5361    }
5362
5363    return $code;
5364}
5365
5366/** @param iterable<FuncInfo> $funcInfos */
5367function generateFunctionAttributeInitialization(iterable $funcInfos, array $allConstInfos, ?int $phpVersionIdMinimumCompatibility, ?string $parentCond = null): string {
5368    return generateCodeWithConditions(
5369        $funcInfos,
5370        "",
5371        static function (FuncInfo $funcInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility) {
5372            $code = null;
5373
5374            if ($funcInfo->name instanceof MethodName) {
5375                $functionTable = "&class_entry->function_table";
5376            } else {
5377                $functionTable = "CG(function_table)";
5378            }
5379
5380            foreach ($funcInfo->attributes as $key => $attribute) {
5381                $code .= $attribute->generateCode(
5382                    "zend_add_function_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1)",
5383                    "func_" . $funcInfo->name->getNameForAttributes() . "_$key",
5384                    $allConstInfos,
5385                    $phpVersionIdMinimumCompatibility
5386                );
5387            }
5388
5389            foreach ($funcInfo->args as $index => $arg) {
5390                foreach ($arg->attributes as $key => $attribute) {
5391                    $code .= $attribute->generateCode(
5392                        "zend_add_parameter_attribute(zend_hash_str_find_ptr($functionTable, \"" . $funcInfo->name->getNameForAttributes() . "\", sizeof(\"" . $funcInfo->name->getNameForAttributes() . "\") - 1), $index",
5393                        "func_{$funcInfo->name->getNameForAttributes()}_arg{$index}_$key",
5394                        $allConstInfos,
5395                        $phpVersionIdMinimumCompatibility
5396                    );
5397                }
5398            }
5399
5400            return $code;
5401        },
5402        $parentCond
5403    );
5404}
5405
5406/**
5407 * @param iterable<ConstInfo> $constInfos
5408 * @param array<string, ConstInfo> $allConstInfos
5409 */
5410function generateConstantAttributeInitialization(
5411    iterable $constInfos,
5412    array $allConstInfos,
5413    ?int $phpVersionIdMinimumCompatibility,
5414    ?string $parentCond = null
5415): string {
5416    return generateCodeWithConditions(
5417        $constInfos,
5418        "",
5419        static function (ConstInfo $constInfo) use ($allConstInfos, $phpVersionIdMinimumCompatibility) {
5420            $code = null;
5421
5422            foreach ($constInfo->attributes as $key => $attribute) {
5423                $code .= $attribute->generateCode(
5424                    "zend_add_class_constant_attribute(class_entry, const_" . $constInfo->name->getDeclarationName(),
5425                    "const_" . $constInfo->name->getDeclarationName() . "_$key",
5426                    $allConstInfos,
5427                    $phpVersionIdMinimumCompatibility
5428                );
5429            }
5430
5431            return $code;
5432        },
5433        $parentCond
5434    );
5435}
5436
5437/**
5438 * @param iterable<PropertyInfo> $propertyInfos
5439 * @param array<string, ConstInfo> $allConstInfos
5440 */
5441function generatePropertyAttributeInitialization(
5442    iterable $propertyInfos,
5443    array $allConstInfos,
5444    ?int $phpVersionIdMinimumCompatibility
5445): string {
5446    $code = "";
5447    foreach ($propertyInfos as $propertyInfo) {
5448        foreach ($propertyInfo->attributes as $key => $attribute) {
5449            $code .= $attribute->generateCode(
5450                "zend_add_property_attribute(class_entry, property_" . $propertyInfo->name->getDeclarationName(),
5451                "property_" . $propertyInfo->name->getDeclarationName() . "_" . $key,
5452                $allConstInfos,
5453                $phpVersionIdMinimumCompatibility
5454            );
5455        }
5456    }
5457
5458    return $code;
5459}
5460
5461/** @param array<string, FuncInfo> $funcMap */
5462function generateOptimizerInfo(array $funcMap): string {
5463
5464    $code = "/* This is a generated file, edit the .stub.php files instead. */\n\n";
5465
5466    $code .= "static const func_info_t func_infos[] = {\n";
5467
5468    $code .= generateCodeWithConditions($funcMap, "", static function (FuncInfo $funcInfo) {
5469        return $funcInfo->getOptimizerInfo();
5470    });
5471
5472    $code .= "};\n";
5473
5474    return $code;
5475}
5476
5477/**
5478 * @param array<int, string[]> $flagsByPhpVersions
5479 * @return string[]
5480 */
5481function generateVersionDependentFlagCode(string $codeTemplate, array $flagsByPhpVersions, ?int $phpVersionIdMinimumCompatibility): array
5482{
5483    $phpVersions = ALL_PHP_VERSION_IDS;
5484    sort($phpVersions);
5485    $currentPhpVersion = end($phpVersions);
5486
5487    // No version compatibility is needed
5488    if ($phpVersionIdMinimumCompatibility === null) {
5489        if (empty($flagsByPhpVersions[$currentPhpVersion])) {
5490            return [];
5491        }
5492
5493        return [sprintf($codeTemplate, implode("|", $flagsByPhpVersions[$currentPhpVersion]))];
5494    }
5495
5496    // Remove flags which depend on a PHP version below the minimally supported one
5497    ksort($flagsByPhpVersions);
5498    $index = array_search($phpVersionIdMinimumCompatibility, array_keys($flagsByPhpVersions));
5499    if ($index === false) {
5500        throw new Exception("Missing version dependent flags for PHP version ID \"$phpVersionIdMinimumCompatibility\"");
5501    }
5502    $flagsByPhpVersions = array_slice($flagsByPhpVersions, $index, null, true);
5503
5504    // Remove empty version-specific flags
5505    $flagsByPhpVersions = array_filter(
5506        $flagsByPhpVersions,
5507        static function (array $value): bool {
5508            return !empty($value);
5509    });
5510
5511    // There are no version-specific flags
5512    if (empty($flagsByPhpVersions)) {
5513        return [];
5514    }
5515
5516    // Remove version-specific flags which don't differ from the previous one
5517    $previousVersionId = null;
5518    foreach ($flagsByPhpVersions as $versionId => $versionFlags) {
5519        if ($previousVersionId !== null && $flagsByPhpVersions[$previousVersionId] === $versionFlags) {
5520            unset($flagsByPhpVersions[$versionId]);
5521        } else {
5522            $previousVersionId = $versionId;
5523        }
5524    }
5525
5526    $flagCount = count($flagsByPhpVersions);
5527
5528    // Do not add a condition unnecessarily when the only version is the same as the minimally supported one
5529    if ($flagCount === 1) {
5530        reset($flagsByPhpVersions);
5531        $firstVersion = key($flagsByPhpVersions);
5532        if ($firstVersion === $phpVersionIdMinimumCompatibility) {
5533            return [sprintf($codeTemplate, implode("|", reset($flagsByPhpVersions)))];
5534        }
5535    }
5536
5537    // Add the necessary conditions around the code using the version-specific flags
5538    $result = [];
5539    $i = 0;
5540    foreach (array_reverse($flagsByPhpVersions, true) as $version => $versionFlags) {
5541        $code = "";
5542
5543        $if = $i === 0 ? "#if" : "#elif";
5544        $endif = $i === $flagCount - 1 ? "#endif\n" : "";
5545
5546        $code .= "$if (PHP_VERSION_ID >= $version)\n";
5547
5548        $code .= sprintf($codeTemplate, implode("|", $versionFlags));
5549        $code .= $endif;
5550
5551        $result[] = $code;
5552        $i++;
5553    }
5554
5555    return $result;
5556}
5557
5558/**
5559 * @param array<string, ConstInfo> $constMap
5560 * @param array<string, ConstInfo> $undocumentedConstMap
5561 * @return array<string, string|null>
5562 */
5563function replacePredefinedConstants(string $targetDirectory, array $constMap, array &$undocumentedConstMap): array {
5564    /** @var array<string, string> $documentedConstMap */
5565    $documentedConstMap = [];
5566    /** @var array<string, string> $predefinedConstants */
5567    $predefinedConstants = [];
5568
5569    $it = new RecursiveIteratorIterator(
5570        new RecursiveDirectoryIterator($targetDirectory),
5571        RecursiveIteratorIterator::LEAVES_ONLY
5572    );
5573
5574    foreach ($it as $file) {
5575        $pathName = $file->getPathName();
5576        if (!preg_match('/(?:[\w\.]*constants[\w\._]*|tokens).xml$/i', basename($pathName))) {
5577            continue;
5578        }
5579
5580        $xml = file_get_contents($pathName);
5581        if ($xml === false) {
5582            continue;
5583        }
5584
5585        if (stripos($xml, "<appendix") === false && stripos($xml, "<sect2") === false &&
5586            stripos($xml, "<chapter") === false && stripos($xml, 'role="constant_list"') === false
5587        ) {
5588            continue;
5589        }
5590
5591        $replacedXml = getReplacedSynopsisXml($xml);
5592
5593        $doc = new DOMDocument();
5594        $doc->formatOutput = false;
5595        $doc->preserveWhiteSpace = true;
5596        $doc->validateOnParse = true;
5597        $success = $doc->loadXML($replacedXml);
5598        if (!$success) {
5599            echo "Failed opening $pathName\n";
5600            continue;
5601        }
5602
5603        $updated = false;
5604
5605        foreach ($doc->getElementsByTagName("varlistentry") as $entry) {
5606            if (!$entry instanceof DOMElement) {
5607                continue;
5608            }
5609
5610            foreach ($entry->getElementsByTagName("term") as $manualTermElement) {
5611                $manualConstantElement = $manualTermElement->getElementsByTagName("constant")->item(0);
5612                if (!$manualConstantElement instanceof DOMElement) {
5613                    continue;
5614                }
5615
5616                $manualConstantName = $manualConstantElement->textContent;
5617
5618                $stubConstant = $constMap[$manualConstantName] ?? null;
5619                if ($stubConstant === null) {
5620                    continue;
5621                }
5622
5623                $documentedConstMap[$manualConstantName] = $manualConstantName;
5624
5625                if ($entry->firstChild instanceof DOMText) {
5626                    $indentationLevel = strlen(str_replace("\n", "", $entry->firstChild->textContent));
5627                } else {
5628                    $indentationLevel = 3;
5629                }
5630                $newTermElement = $stubConstant->getPredefinedConstantTerm($doc, $indentationLevel);
5631
5632                if ($manualTermElement->textContent === $newTermElement->textContent) {
5633                    continue;
5634                }
5635
5636                $manualTermElement->parentNode->replaceChild($newTermElement, $manualTermElement);
5637                $updated = true;
5638            }
5639        }
5640
5641        foreach ($doc->getElementsByTagName("row") as $row) {
5642            if (!$row instanceof DOMElement) {
5643                continue;
5644            }
5645
5646            $entry = $row->getElementsByTagName("entry")->item(0);
5647            if (!$entry instanceof DOMElement) {
5648                continue;
5649            }
5650
5651            foreach ($entry->getElementsByTagName("constant") as $manualConstantElement) {
5652                if (!$manualConstantElement instanceof DOMElement) {
5653                    continue;
5654                }
5655
5656                $manualConstantName = $manualConstantElement->textContent;
5657
5658                $stubConstant = $constMap[$manualConstantName] ?? null;
5659                if ($stubConstant === null) {
5660                    continue;
5661                }
5662
5663                $documentedConstMap[$manualConstantName] = $manualConstantName;
5664
5665                if ($row->firstChild instanceof DOMText) {
5666                    $indentationLevel = strlen(str_replace("\n", "", $row->firstChild->textContent));
5667                } else {
5668                    $indentationLevel = 3;
5669                }
5670                $newEntryElement = $stubConstant->getPredefinedConstantEntry($doc, $indentationLevel);
5671
5672                if ($entry->textContent === $newEntryElement->textContent) {
5673                    continue;
5674                }
5675
5676                $entry->parentNode->replaceChild($newEntryElement, $entry);
5677                $updated = true;
5678            }
5679        }
5680
5681        if ($updated) {
5682            $replacedXml = $doc->saveXML();
5683
5684            $replacedXml = preg_replace(
5685                [
5686                    "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/",
5687                    '/<appendix\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5688                    '/<appendix\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5689                    '/<sect2\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5690                    '/<sect2\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5691                    '/<chapter\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5692                    '/<chapter\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5693                ],
5694                [
5695                    "&$1",
5696                    "<appendix xml:id=\"$2\" xmlns=\"$1\">",
5697                    "<appendix xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">",
5698                    "<sect2 xml:id=\"$2\" xmlns=\"$1\">",
5699                    "<sect2 xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">",
5700                    "<chapter xml:id=\"$2\" xmlns=\"$1\">",
5701                    "<chapter xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">",
5702                ],
5703                $replacedXml
5704            );
5705
5706            $predefinedConstants[$pathName] = $replacedXml;
5707        }
5708    }
5709
5710    $undocumentedConstMap = array_diff_key($constMap, $documentedConstMap);
5711
5712    return $predefinedConstants;
5713}
5714
5715/**
5716 * @param array<string, ClassInfo> $classMap
5717 * @param array<string, ConstInfo> $allConstInfos
5718 * @return array<string, string>
5719 */
5720function generateClassSynopses(array $classMap, array $allConstInfos): array {
5721    $result = [];
5722
5723    foreach ($classMap as $classInfo) {
5724        $classSynopsis = $classInfo->getClassSynopsisDocument($classMap, $allConstInfos);
5725        if ($classSynopsis !== null) {
5726            $result[ClassInfo::getClassSynopsisFilename($classInfo->name) . ".xml"] = $classSynopsis;
5727        }
5728    }
5729
5730    return $result;
5731}
5732
5733/**
5734 * @param array<string, ClassInfo> $classMap
5735 * @param array<string, ConstInfo> $allConstInfos
5736 * @param array<string, ClassInfo> $undocumentedClassMap
5737 * @return array<string, string>
5738 */
5739function replaceClassSynopses(
5740    string $targetDirectory,
5741    array $classMap,
5742    array $allConstInfos,
5743    array &$undocumentedClassMap
5744): array {
5745    /** @var array<string, string> $documentedClassMap */
5746    $documentedClassMap = [];
5747    /** @var array<string, string> $classSynopses */
5748    $classSynopses = [];
5749
5750    $it = new RecursiveIteratorIterator(
5751        new RecursiveDirectoryIterator($targetDirectory),
5752        RecursiveIteratorIterator::LEAVES_ONLY
5753    );
5754
5755    foreach ($it as $file) {
5756        $pathName = $file->getPathName();
5757        if (!preg_match('/\.xml$/i', $pathName)) {
5758            continue;
5759        }
5760
5761        $xml = file_get_contents($pathName);
5762        if ($xml === false) {
5763            continue;
5764        }
5765
5766        if (stripos($xml, "<classsynopsis") === false) {
5767            continue;
5768        }
5769
5770        $replacedXml = getReplacedSynopsisXml($xml);
5771
5772        $doc = new DOMDocument();
5773        $doc->formatOutput = false;
5774        $doc->preserveWhiteSpace = true;
5775        $doc->validateOnParse = true;
5776        $success = $doc->loadXML($replacedXml);
5777        if (!$success) {
5778            echo "Failed opening $pathName\n";
5779            continue;
5780        }
5781
5782        $classSynopsisElements = [];
5783        foreach ($doc->getElementsByTagName("classsynopsis") as $element) {
5784            $classSynopsisElements[] = $element;
5785        }
5786
5787        foreach ($classSynopsisElements as $classSynopsis) {
5788            if (!$classSynopsis instanceof DOMElement) {
5789                continue;
5790            }
5791
5792            $child = $classSynopsis->firstElementChild;
5793            if ($child === null) {
5794                continue;
5795            }
5796            $child = $child->lastElementChild;
5797            if ($child === null) {
5798                continue;
5799            }
5800            $className = $child->textContent;
5801            if (!isset($classMap[$className])) {
5802                continue;
5803            }
5804
5805            $documentedClassMap[$className] = $className;
5806
5807            $classInfo = $classMap[$className];
5808
5809            $newClassSynopsis = $classInfo->getClassSynopsisElement($doc, $classMap, $allConstInfos);
5810            if ($newClassSynopsis === null) {
5811                continue;
5812            }
5813
5814            // Check if there is any change - short circuit if there is not any.
5815
5816            if (replaceAndCompareXmls($doc, $classSynopsis, $newClassSynopsis)) {
5817                continue;
5818            }
5819
5820            // Return the updated XML
5821
5822            $replacedXml = $doc->saveXML();
5823
5824            $replacedXml = preg_replace(
5825                [
5826                    "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/",
5827                    '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5828                    '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5829                    '/<reference\s+role="(\w+)"\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5830                    '/<reference\s+role="(\w+)"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
5831                    '/<reference\s+xmlns=\"([^"]+)\"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+role="(\w+)"\s+xml:id="([^"]+)"\s*>/i',
5832                    '/<reference\s+xmlns=\"([^"]+)\"\s+xmlns:xlink="([^"]+)"\s+xmlns:xi="([^"]+)"\s+xml:id="([^"]+)"\s+role="(\w+)"\s*>/i',
5833                ],
5834                [
5835                    "&$1",
5836                    "<reference xml:id=\"$3\" role=\"$1\" xmlns=\"$2\">",
5837                    "<reference xml:id=\"$4\" role=\"$1\" xmlns=\"$2\" xmlns:xi=\"$3\">",
5838                    "<reference xml:id=\"$5\" role=\"$1\" xmlns=\"$2\" xmlns:xlink=\"$3\" xmlns:xi=\"$4\">",
5839                    "<reference xml:id=\"$5\" role=\"$1\" xmlns=\"$4\" xmlns:xlink=\"$2\" xmlns:xi=\"$2\">",
5840                    "<reference xml:id=\"$5\" role=\"$4\" xmlns=\"$1\" xmlns:xlink=\"$2\" xmlns:xi=\"$3\">",
5841                    "<reference xml:id=\"$4\" role=\"$5\" xmlns=\"$1\" xmlns:xlink=\"$2\" xmlns:xi=\"$3\">",
5842                ],
5843                $replacedXml
5844            );
5845
5846            $classSynopses[$pathName] = $replacedXml;
5847        }
5848    }
5849
5850    $undocumentedClassMap = array_diff_key($classMap, $documentedClassMap);
5851
5852    return $classSynopses;
5853}
5854
5855function getReplacedSynopsisXml(string $xml): string
5856{
5857    return preg_replace(
5858        [
5859            "/&([A-Za-z0-9._{}%-]+?;)/",
5860            "/<(\/)*xi:([A-Za-z]+?)/"
5861        ],
5862        [
5863            "REPLACED-ENTITY-$1",
5864            "<$1XI$2",
5865        ],
5866        $xml
5867    );
5868}
5869
5870/**
5871 * @param array<string, FuncInfo> $funcMap
5872 * @param array<string, FuncInfo> $aliasMap
5873 * @return array<string, string>
5874 */
5875function generateMethodSynopses(array $funcMap, array $aliasMap): array {
5876    $result = [];
5877
5878    foreach ($funcMap as $funcInfo) {
5879        $methodSynopsis = $funcInfo->getMethodSynopsisDocument($funcMap, $aliasMap);
5880        if ($methodSynopsis !== null) {
5881            $result[$funcInfo->name->getMethodSynopsisFilename() . ".xml"] = $methodSynopsis;
5882        }
5883    }
5884
5885    return $result;
5886}
5887
5888/**
5889 * @param array<string, FuncInfo> $funcMap
5890 * @param array<string, FuncInfo> $aliasMap
5891 * @param array<int, string> $methodSynopsisWarnings
5892 * @param array<string, FuncInfo> $undocumentedFuncMap
5893 * @return array<string, string>
5894 */
5895function replaceMethodSynopses(
5896    string $targetDirectory,
5897    array $funcMap,
5898    array $aliasMap,
5899    bool $isVerifyManual,
5900    array &$methodSynopsisWarnings,
5901    array &$undocumentedFuncMap
5902): array {
5903    /** @var array<string, string> $documentedFuncMap */
5904    $documentedFuncMap = [];
5905    /** @var array<string, string> $methodSynopses */
5906    $methodSynopses = [];
5907
5908    $it = new RecursiveIteratorIterator(
5909        new RecursiveDirectoryIterator($targetDirectory),
5910        RecursiveIteratorIterator::LEAVES_ONLY
5911    );
5912
5913    foreach ($it as $file) {
5914        $pathName = $file->getPathName();
5915        if (!preg_match('/\.xml$/i', $pathName)) {
5916            continue;
5917        }
5918
5919        $xml = file_get_contents($pathName);
5920        if ($xml === false) {
5921            continue;
5922        }
5923
5924        if ($isVerifyManual) {
5925            $matches = [];
5926            preg_match("/<refname>\s*([\w:]+)\s*<\/refname>\s*<refpurpose>\s*&Alias;\s*<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>\s*<\/refpurpose>/i", $xml, $matches);
5927            $aliasName = $matches[1] ?? null;
5928            $alias = $funcMap[$aliasName] ?? null;
5929            $funcName = $matches[2] ?? null;
5930            $func = $funcMap[$funcName] ?? null;
5931
5932            if ($alias &&
5933                !$alias->isUndocumentable &&
5934                ($func === null || $func->alias === null || $func->alias->__toString() !== $aliasName) &&
5935                ($alias->alias === null || $alias->alias->__toString() !== $funcName)
5936            ) {
5937                $methodSynopsisWarnings[] = "$aliasName()" . ($alias->alias ? " is an alias of " . $alias->alias->__toString() . "(), but it" : "") . " is incorrectly documented as an alias for $funcName()";
5938            }
5939
5940            $matches = [];
5941            preg_match("/<(?:para|simpara)>\s*(?:&info.function.alias;|&info.method.alias;|&Alias;)\s+<(?:function|methodname)>\s*([\w:]+)\s*<\/(?:function|methodname)>/i", $xml, $matches);
5942            $descriptionFuncName = $matches[1] ?? null;
5943            $descriptionFunc = $funcMap[$descriptionFuncName] ?? null;
5944            if ($descriptionFunc && $funcName !== $descriptionFuncName) {
5945                $methodSynopsisWarnings[] = "Alias in the method synopsis description of $pathName doesn't match the alias in the <refpurpose>";
5946            }
5947
5948            if ($aliasName) {
5949                $documentedFuncMap[$aliasName] = $aliasName;
5950            }
5951        }
5952
5953        if (stripos($xml, "<methodsynopsis") === false && stripos($xml, "<constructorsynopsis") === false && stripos($xml, "<destructorsynopsis") === false) {
5954            continue;
5955        }
5956
5957        $replacedXml = getReplacedSynopsisXml($xml);
5958
5959        $doc = new DOMDocument();
5960        $doc->formatOutput = false;
5961        $doc->preserveWhiteSpace = true;
5962        $doc->validateOnParse = true;
5963        $success = $doc->loadXML($replacedXml);
5964        if (!$success) {
5965            echo "Failed opening $pathName\n";
5966            continue;
5967        }
5968
5969        $methodSynopsisElements = [];
5970        foreach ($doc->getElementsByTagName("constructorsynopsis") as $element) {
5971            $methodSynopsisElements[] = $element;
5972        }
5973        foreach ($doc->getElementsByTagName("destructorsynopsis") as $element) {
5974            $methodSynopsisElements[] = $element;
5975        }
5976        foreach ($doc->getElementsByTagName("methodsynopsis") as $element) {
5977            $methodSynopsisElements[] = $element;
5978        }
5979
5980        foreach ($methodSynopsisElements as $methodSynopsis) {
5981            if (!$methodSynopsis instanceof DOMElement) {
5982                continue;
5983            }
5984
5985            $item = $methodSynopsis->getElementsByTagName("methodname")->item(0);
5986            if (!$item instanceof DOMElement) {
5987                continue;
5988            }
5989            $funcName = $item->textContent;
5990            if (!isset($funcMap[$funcName])) {
5991                continue;
5992            }
5993
5994            $funcInfo = $funcMap[$funcName];
5995            $documentedFuncMap[$funcInfo->name->__toString()] = $funcInfo->name->__toString();
5996
5997            $newMethodSynopsis = $funcInfo->getMethodSynopsisElement($funcMap, $aliasMap, $doc);
5998            if ($newMethodSynopsis === null) {
5999                continue;
6000            }
6001
6002            // Retrieve current signature
6003
6004            $params = [];
6005            $list = $methodSynopsis->getElementsByTagName("methodparam");
6006            foreach ($list as $i => $item) {
6007                if (!$item instanceof DOMElement) {
6008                    continue;
6009                }
6010
6011                $paramList = $item->getElementsByTagName("parameter");
6012                if ($paramList->count() !== 1) {
6013                    continue;
6014                }
6015
6016                $paramName = $paramList->item(0)->textContent;
6017                $paramTypes = [];
6018
6019                $paramList = $item->getElementsByTagName("type");
6020                foreach ($paramList as $type) {
6021                    if (!$type instanceof DOMElement) {
6022                        continue;
6023                    }
6024
6025                    $paramTypes[] = $type->textContent;
6026                }
6027
6028                $params[$paramName] = ["index" => $i, "type" => $paramTypes];
6029            }
6030
6031            // Check if there is any change - short circuit if there is not any.
6032
6033            if (replaceAndCompareXmls($doc, $methodSynopsis, $newMethodSynopsis)) {
6034                continue;
6035            }
6036
6037            // Update parameter references
6038
6039            $paramList = $doc->getElementsByTagName("parameter");
6040            /** @var DOMElement $paramElement */
6041            foreach ($paramList as $paramElement) {
6042                if ($paramElement->parentNode && $paramElement->parentNode->nodeName === "methodparam") {
6043                    continue;
6044                }
6045
6046                $name = $paramElement->textContent;
6047                if (!isset($params[$name])) {
6048                    continue;
6049                }
6050
6051                $index = $params[$name]["index"];
6052                if (!isset($funcInfo->args[$index])) {
6053                    continue;
6054                }
6055
6056                $paramElement->textContent = $funcInfo->args[$index]->name;
6057            }
6058
6059            // Return the updated XML
6060
6061            $replacedXml = $doc->saveXML();
6062
6063            $replacedXml = preg_replace(
6064                [
6065                    "/REPLACED-ENTITY-([A-Za-z0-9._{}%-]+?;)/",
6066                    '/<refentry\s+xmlns="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
6067                    '/<refentry\s+xmlns="([^"]+)"\s+xmlns:xlink="([^"]+)"\s+xml:id="([^"]+)"\s*>/i',
6068                ],
6069                [
6070                    "&$1",
6071                    "<refentry xml:id=\"$2\" xmlns=\"$1\">",
6072                    "<refentry xml:id=\"$3\" xmlns=\"$1\" xmlns:xlink=\"$2\">",
6073                ],
6074                $replacedXml
6075            );
6076
6077            $methodSynopses[$pathName] = $replacedXml;
6078        }
6079    }
6080
6081    $undocumentedFuncMap = array_diff_key($funcMap, $documentedFuncMap);
6082
6083    return $methodSynopses;
6084}
6085
6086function replaceAndCompareXmls(DOMDocument $doc, DOMElement $originalSynopsis, DOMElement $newSynopsis): bool
6087{
6088    $docComparator = new DOMDocument();
6089    $docComparator->preserveWhiteSpace = false;
6090    $docComparator->formatOutput = true;
6091
6092    $xml1 = $doc->saveXML($originalSynopsis);
6093    $xml1 = getReplacedSynopsisXml($xml1);
6094    $docComparator->loadXML($xml1);
6095    $xml1 = $docComparator->saveXML();
6096
6097    $originalSynopsis->parentNode->replaceChild($newSynopsis, $originalSynopsis);
6098
6099    $xml2 = $doc->saveXML($newSynopsis);
6100    $xml2 = getReplacedSynopsisXml($xml2);
6101
6102    $docComparator->loadXML($xml2);
6103    $xml2 = $docComparator->saveXML();
6104
6105    return $xml1 === $xml2;
6106}
6107
6108function installPhpParser(string $version, string $phpParserDir) {
6109    $lockFile = __DIR__ . "/PHP-Parser-install-lock";
6110    $lockFd = fopen($lockFile, 'w+');
6111    if (!flock($lockFd, LOCK_EX)) {
6112        throw new Exception("Failed to acquire installation lock");
6113    }
6114
6115    try {
6116        // Check whether a parallel process has already installed PHP-Parser.
6117        if (is_dir($phpParserDir)) {
6118            return;
6119        }
6120
6121        $cwd = getcwd();
6122        chdir(__DIR__);
6123
6124        $tarName = "v$version.tar.gz";
6125        passthru("wget https://github.com/nikic/PHP-Parser/archive/$tarName", $exit);
6126        if ($exit !== 0) {
6127            passthru("curl -LO https://github.com/nikic/PHP-Parser/archive/$tarName", $exit);
6128        }
6129        if ($exit !== 0) {
6130            throw new Exception("Failed to download PHP-Parser tarball");
6131        }
6132        if (!mkdir($phpParserDir)) {
6133            throw new Exception("Failed to create directory $phpParserDir");
6134        }
6135        passthru("tar xvzf $tarName -C PHP-Parser-$version --strip-components 1", $exit);
6136        if ($exit !== 0) {
6137            throw new Exception("Failed to extract PHP-Parser tarball");
6138        }
6139        unlink(__DIR__ . "/$tarName");
6140        chdir($cwd);
6141    } finally {
6142        flock($lockFd, LOCK_UN);
6143        @unlink($lockFile);
6144    }
6145}
6146
6147function initPhpParser() {
6148    static $isInitialized = false;
6149    if ($isInitialized) {
6150        return;
6151    }
6152
6153    if (!extension_loaded("tokenizer")) {
6154        throw new Exception("The \"tokenizer\" extension is not available");
6155    }
6156
6157    $isInitialized = true;
6158    $version = "5.3.1";
6159    $phpParserDir = __DIR__ . "/PHP-Parser-$version";
6160    if (!is_dir($phpParserDir)) {
6161        installPhpParser($version, $phpParserDir);
6162    }
6163
6164    spl_autoload_register(static function(string $class) use ($phpParserDir) {
6165        if (strpos($class, "PhpParser\\") === 0) {
6166            $fileName = $phpParserDir . "/lib/" . str_replace("\\", "/", $class) . ".php";
6167            require $fileName;
6168        }
6169    });
6170}
6171
6172$optind = null;
6173$options = getopt(
6174    "fh",
6175    [
6176        "force-regeneration", "parameter-stats", "help", "verify", "verify-manual", "replace-predefined-constants",
6177        "generate-classsynopses", "replace-classsynopses", "generate-methodsynopses", "replace-methodsynopses",
6178        "generate-optimizer-info",
6179    ],
6180    $optind
6181);
6182
6183$context = new Context;
6184$printParameterStats = isset($options["parameter-stats"]);
6185$verify = isset($options["verify"]);
6186$verifyManual = isset($options["verify-manual"]);
6187$replacePredefinedConstants = isset($options["replace-predefined-constants"]);
6188$generateClassSynopses = isset($options["generate-classsynopses"]);
6189$replaceClassSynopses = isset($options["replace-classsynopses"]);
6190$generateMethodSynopses = isset($options["generate-methodsynopses"]);
6191$replaceMethodSynopses = isset($options["replace-methodsynopses"]);
6192$generateOptimizerInfo = isset($options["generate-optimizer-info"]);
6193$context->forceRegeneration = isset($options["f"]) || isset($options["force-regeneration"]);
6194$context->forceParse = $context->forceRegeneration || $printParameterStats || $verify || $verifyManual || $replacePredefinedConstants || $generateClassSynopses || $generateOptimizerInfo || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses;
6195
6196if (isset($options["h"]) || isset($options["help"])) {
6197    die("\nUsage: gen_stub.php [ -f | --force-regeneration ] [ --replace-predefined-constants ] [ --generate-classsynopses ] [ --replace-classsynopses ] [ --generate-methodsynopses ] [ --replace-methodsynopses ] [ --parameter-stats ] [ --verify ]  [ --verify-manual ] [ --generate-optimizer-info ] [ -h | --help ] [ name.stub.php | directory ] [ directory ]\n\n");
6198}
6199
6200$locations = array_slice($argv, $optind);
6201$locationCount = count($locations);
6202if ($replacePredefinedConstants && $locationCount < 2) {
6203    die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-predefined-constants ./ ../doc-en/\n");
6204}
6205if ($replaceClassSynopses && $locationCount < 2) {
6206    die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-classsynopses ./ ../doc-en/\n");
6207}
6208if ($generateMethodSynopses && $locationCount < 2) {
6209    die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --generate-methodsynopses ./ ../doc-en/\n");
6210}
6211if ($replaceMethodSynopses && $locationCount < 2) {
6212    die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --replace-methodsynopses ./ ../doc-en/\n");
6213}
6214if ($verifyManual && $locationCount < 2) {
6215    die("At least one source stub path and a target manual directory has to be provided:\n./build/gen_stub.php --verify-manual ./ ../doc-en/\n");
6216}
6217$manualTarget = null;
6218if ($replacePredefinedConstants || $replaceClassSynopses || $generateMethodSynopses || $replaceMethodSynopses || $verifyManual) {
6219    $manualTarget = array_pop($locations);
6220}
6221if ($locations === []) {
6222    $locations = ['.'];
6223}
6224
6225$fileInfos = [];
6226foreach (array_unique($locations) as $location) {
6227    if (is_file($location)) {
6228        // Generate single file.
6229        $fileInfo = processStubFile($location, $context);
6230        if ($fileInfo) {
6231            $fileInfos[] = $fileInfo;
6232        }
6233    } else if (is_dir($location)) {
6234        array_push($fileInfos, ...processDirectory($location, $context));
6235    } else {
6236        echo "$location is neither a file nor a directory.\n";
6237        exit(1);
6238    }
6239}
6240
6241if ($printParameterStats) {
6242    $parameterStats = [];
6243
6244    foreach ($fileInfos as $fileInfo) {
6245        foreach ($fileInfo->getAllFuncInfos() as $funcInfo) {
6246            foreach ($funcInfo->args as $argInfo) {
6247                if (!isset($parameterStats[$argInfo->name])) {
6248                    $parameterStats[$argInfo->name] = 0;
6249                }
6250                $parameterStats[$argInfo->name]++;
6251            }
6252        }
6253    }
6254
6255    arsort($parameterStats);
6256    echo json_encode($parameterStats, JSON_PRETTY_PRINT), "\n";
6257}
6258
6259/** @var array<string, ClassInfo> $classMap */
6260$classMap = [];
6261/** @var array<string, FuncInfo> $funcMap */
6262$funcMap = [];
6263/** @var array<string, FuncInfo> $aliasMap */
6264$aliasMap = [];
6265
6266/** @var array<string, ConstInfo> $undocumentedConstMap */
6267$undocumentedConstMap = [];
6268/** @var array<string, ClassInfo> $undocumentedClassMap */
6269$undocumentedClassMap = [];
6270/** @var array<string, FuncInfo> $undocumentedFuncMap */
6271$undocumentedFuncMap = [];
6272/** @var array<int, string> $methodSynopsisWarnings */
6273$methodSynopsisWarnings = [];
6274
6275foreach ($fileInfos as $fileInfo) {
6276    foreach ($fileInfo->getAllFuncInfos() as $funcInfo) {
6277        $funcMap[$funcInfo->name->__toString()] = $funcInfo;
6278
6279        // TODO: Don't use aliasMap for methodsynopsis?
6280        if ($funcInfo->aliasType === "alias") {
6281            $aliasMap[$funcInfo->alias->__toString()] = $funcInfo;
6282        }
6283    }
6284
6285    foreach ($fileInfo->classInfos as $classInfo) {
6286        $classMap[$classInfo->name->__toString()] = $classInfo;
6287
6288        if ($classInfo->alias !== null) {
6289            $classMap[$classInfo->alias] = $classInfo;
6290        }
6291    }
6292}
6293
6294if ($verify) {
6295    $errors = [];
6296
6297    foreach ($funcMap as $aliasFunc) {
6298        if (!$aliasFunc->alias || $aliasFunc->aliasType !== "alias") {
6299            continue;
6300        }
6301
6302        if (!isset($funcMap[$aliasFunc->alias->__toString()])) {
6303            $errors[] = "Aliased function {$aliasFunc->alias}() cannot be found";
6304            continue;
6305        }
6306
6307        if (!$aliasFunc->verify) {
6308            continue;
6309        }
6310
6311        $aliasedFunc = $funcMap[$aliasFunc->alias->__toString()];
6312        $aliasedArgs = $aliasedFunc->args;
6313        $aliasArgs = $aliasFunc->args;
6314
6315        if ($aliasFunc->isInstanceMethod() !== $aliasedFunc->isInstanceMethod()) {
6316            if ($aliasFunc->isInstanceMethod()) {
6317                $aliasedArgs = array_slice($aliasedArgs, 1);
6318            }
6319
6320            if ($aliasedFunc->isInstanceMethod()) {
6321                $aliasArgs = array_slice($aliasArgs, 1);
6322            }
6323        }
6324
6325        array_map(
6326            function(?ArgInfo $aliasArg, ?ArgInfo $aliasedArg) use ($aliasFunc, $aliasedFunc, &$errors) {
6327                if ($aliasArg === null) {
6328                    assert($aliasedArg !== null);
6329                    $errors[] = "{$aliasFunc->name}(): Argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() is missing";
6330                    return null;
6331                }
6332
6333                if ($aliasedArg === null) {
6334                    $errors[] = "{$aliasedFunc->name}(): Argument \$$aliasArg->name of alias function {$aliasFunc->name}() is missing";
6335                    return null;
6336                }
6337
6338                if ($aliasArg->name !== $aliasedArg->name) {
6339                    $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same name";
6340                    return null;
6341                }
6342
6343                if ($aliasArg->type != $aliasedArg->type) {
6344                    $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same type";
6345                }
6346
6347                if ($aliasArg->defaultValue !== $aliasedArg->defaultValue) {
6348                    $errors[] = "{$aliasFunc->name}(): Argument \$$aliasArg->name and argument \$$aliasedArg->name of aliased function {$aliasedFunc->name}() must have the same default value";
6349                }
6350            },
6351            $aliasArgs, $aliasedArgs
6352        );
6353
6354        $aliasedReturn = $aliasedFunc->return;
6355        $aliasReturn = $aliasFunc->return;
6356
6357        if (!$aliasedFunc->name->isConstructor() && !$aliasFunc->name->isConstructor()) {
6358            $aliasedReturnType = $aliasedReturn->type ?? $aliasedReturn->phpDocType;
6359            $aliasReturnType = $aliasReturn->type ?? $aliasReturn->phpDocType;
6360            if ($aliasReturnType != $aliasedReturnType) {
6361                $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same return type";
6362            }
6363        }
6364
6365        $aliasedPhpDocReturnType = $aliasedReturn->phpDocType;
6366        $aliasPhpDocReturnType = $aliasReturn->phpDocType;
6367        if ($aliasedPhpDocReturnType != $aliasPhpDocReturnType && $aliasedPhpDocReturnType != $aliasReturn->type && $aliasPhpDocReturnType != $aliasedReturn->type) {
6368            $errors[] = "{$aliasFunc->name}() and {$aliasedFunc->name}() must have the same PHPDoc return type";
6369        }
6370    }
6371
6372    echo implode("\n", $errors);
6373    if (!empty($errors)) {
6374        echo "\n";
6375        exit(1);
6376    }
6377}
6378
6379if ($replacePredefinedConstants || $verifyManual) {
6380    $predefinedConstants = replacePredefinedConstants($manualTarget, $context->allConstInfos, $undocumentedConstMap);
6381
6382    if ($replacePredefinedConstants) {
6383        foreach ($predefinedConstants as $filename => $content) {
6384            if (file_put_contents($filename, $content)) {
6385                echo "Saved $filename\n";
6386            }
6387        }
6388    }
6389}
6390
6391if ($generateClassSynopses) {
6392    $classSynopsesDirectory = getcwd() . "/classsynopses";
6393
6394    $classSynopses = generateClassSynopses($classMap, $context->allConstInfos);
6395    if (!empty($classSynopses)) {
6396        if (!file_exists($classSynopsesDirectory)) {
6397            mkdir($classSynopsesDirectory);
6398        }
6399
6400        foreach ($classSynopses as $filename => $content) {
6401            if (file_put_contents("$classSynopsesDirectory/$filename", $content)) {
6402                echo "Saved $filename\n";
6403            }
6404        }
6405    }
6406}
6407
6408if ($replaceClassSynopses || $verifyManual) {
6409    $classSynopses = replaceClassSynopses($manualTarget, $classMap, $context->allConstInfos, $undocumentedClassMap);
6410
6411    if ($replaceClassSynopses) {
6412        foreach ($classSynopses as $filename => $content) {
6413            if (file_put_contents($filename, $content)) {
6414                echo "Saved $filename\n";
6415            }
6416        }
6417    }
6418}
6419
6420if ($generateMethodSynopses) {
6421    $methodSynopses = generateMethodSynopses($funcMap, $aliasMap);
6422    if (!file_exists($manualTarget)) {
6423        mkdir($manualTarget);
6424    }
6425
6426    foreach ($methodSynopses as $filename => $content) {
6427        $path = "$manualTarget/$filename";
6428
6429        if (!file_exists($path)) {
6430            if (!file_exists(dirname($path))) {
6431                mkdir(dirname($path));
6432            }
6433
6434            if (file_put_contents($path, $content)) {
6435                echo "Saved $filename\n";
6436            }
6437        }
6438    }
6439}
6440
6441if ($replaceMethodSynopses || $verifyManual) {
6442    $methodSynopses = replaceMethodSynopses($manualTarget, $funcMap, $aliasMap, $verifyManual, $methodSynopsisWarnings, $undocumentedFuncMap);
6443
6444    if ($replaceMethodSynopses) {
6445        foreach ($methodSynopses as $filename => $content) {
6446            if (file_put_contents($filename, $content)) {
6447                echo "Saved $filename\n";
6448            }
6449        }
6450    }
6451}
6452
6453if ($generateOptimizerInfo) {
6454    $filename = dirname(__FILE__, 2) . "/Zend/Optimizer/zend_func_infos.h";
6455    $optimizerInfo = generateOptimizerInfo($funcMap);
6456
6457    if (file_put_contents($filename, $optimizerInfo)) {
6458        echo "Saved $filename\n";
6459    }
6460}
6461
6462if ($verifyManual) {
6463    foreach ($undocumentedConstMap as $constName => $info) {
6464        if ($info->name->isClassConst() || $info->isUndocumentable) {
6465            continue;
6466        }
6467
6468        echo "Warning: Missing predefined constant for $constName\n";
6469    }
6470
6471    foreach ($methodSynopsisWarnings as $warning) {
6472        echo "Warning: $warning\n";
6473    }
6474
6475    foreach ($undocumentedClassMap as $className => $info) {
6476        if (!$info->isUndocumentable) {
6477            echo "Warning: Missing class synopsis for $className\n";
6478        }
6479    }
6480
6481    foreach ($undocumentedFuncMap as $functionName => $info) {
6482        if (!$info->isUndocumentable) {
6483            echo "Warning: Missing method synopsis for $functionName()\n";
6484        }
6485    }
6486}
6487