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