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