adb3dc2e2cd19017b85a7668b561c3d3b8a10412
[GitHub/WoltLab/WCF.git] /
1 <?php
2
3 declare(strict_types=1);
4
5 namespace CuyZ\Valinor\Type\Parser\Lexer\Token;
6
7 use CuyZ\Valinor\Type\IntegerType;
8 use CuyZ\Valinor\Type\Parser\Exception\Generic\AssignedGenericNotFound;
9 use CuyZ\Valinor\Type\Parser\Exception\Generic\CannotAssignGeneric;
10 use CuyZ\Valinor\Type\Parser\Exception\Generic\GenericClosingBracketMissing;
11 use CuyZ\Valinor\Type\Parser\Exception\Generic\GenericCommaMissing;
12 use CuyZ\Valinor\Type\Parser\Exception\Generic\InvalidAssignedGeneric;
13 use CuyZ\Valinor\Type\Parser\Exception\Generic\InvalidExtendTagClassName;
14 use CuyZ\Valinor\Type\Parser\Exception\Generic\InvalidExtendTagType;
15 use CuyZ\Valinor\Type\Parser\Exception\Generic\MissingGenerics;
16 use CuyZ\Valinor\Type\Parser\Exception\Generic\ExtendTagTypeError;
17 use CuyZ\Valinor\Type\Parser\Exception\Generic\SeveralExtendTagsFound;
18 use CuyZ\Valinor\Type\Parser\Exception\InvalidType;
19 use CuyZ\Valinor\Type\Parser\Exception\Template\InvalidClassTemplate;
20 use CuyZ\Valinor\Type\Parser\Factory\Specifications\AliasSpecification;
21 use CuyZ\Valinor\Type\Parser\Factory\Specifications\ClassContextSpecification;
22 use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeAliasAssignerSpecification;
23 use CuyZ\Valinor\Type\Parser\Factory\Specifications\TypeParserSpecification;
24 use CuyZ\Valinor\Type\Parser\Factory\TypeParserFactory;
25 use CuyZ\Valinor\Type\Parser\Lexer\TokenStream;
26 use CuyZ\Valinor\Type\Parser\TypeParser;
27 use CuyZ\Valinor\Type\StringType;
28 use CuyZ\Valinor\Type\Type;
29 use CuyZ\Valinor\Type\Types\ArrayKeyType;
30 use CuyZ\Valinor\Type\ClassType;
31 use CuyZ\Valinor\Type\Types\MixedType;
32 use CuyZ\Valinor\Type\Types\NativeClassType;
33 use CuyZ\Valinor\Utility\Reflection\DocParser;
34 use CuyZ\Valinor\Utility\Reflection\Reflection;
35 use ReflectionClass;
36
37 use function array_keys;
38 use function array_shift;
39 use function array_slice;
40 use function count;
41
42 /** @internal */
43 final class AdvancedClassNameToken implements TraversingToken
44 {
45 public function __construct(
46 private ClassNameToken $delegate,
47 private TypeParserFactory $typeParserFactory,
48 ) {}
49
50 public function traverse(TokenStream $stream): Type
51 {
52 $type = $this->delegate->traverse($stream);
53
54 if (! $type instanceof ClassType) {
55 return $type;
56 }
57
58 $className = $type->className();
59 $reflection = Reflection::class($className);
60 $parentReflection = $reflection->getParentClass();
61
62 $specifications = [
63 new ClassContextSpecification($className),
64 new AliasSpecification($reflection),
65 ];
66
67 $templates = $this->templatesTypes($reflection, ...$specifications);
68
69 $generics = $this->generics($stream, $className, $templates);
70 $generics = $this->assignGenerics($className, $templates, $generics);
71
72 if ($parentReflection) {
73 $parserWithGenerics = $this->typeParserFactory->get(new TypeAliasAssignerSpecification($generics), ...$specifications);
74
75 $parentType = $this->parentType($reflection, $parentReflection, $parserWithGenerics);
76 }
77
78 return new NativeClassType($className, $generics, $parentType ?? null);
79 }
80
81 public function symbol(): string
82 {
83 return $this->delegate->symbol();
84 }
85
86 /**
87 * @param ReflectionClass<object> $reflection
88 * @return array<string, Type>
89 */
90 private function templatesTypes(ReflectionClass $reflection, TypeParserSpecification ...$specifications): array
91 {
92 $templates = DocParser::classTemplates($reflection);
93
94 if ($templates === []) {
95 return [];
96 }
97
98 $types = [];
99
100 foreach ($templates as $templateName => $type) {
101 try {
102 if ($type === '') {
103 $types[$templateName] = MixedType::get();
104 } else {
105 /** @infection-ignore-all */
106 $parser ??= $this->typeParserFactory->get(...$specifications);
107
108 $types[$templateName] = $parser->parse($type);
109 }
110 } catch (InvalidType $invalidType) {
111 throw new InvalidClassTemplate($reflection->name, $templateName, $invalidType);
112 }
113 }
114
115 return $types;
116 }
117
118 /**
119 * @param array<string, Type> $templates
120 * @param class-string $className
121 * @return Type[]
122 */
123 private function generics(TokenStream $stream, string $className, array $templates): array
124 {
125 if ($stream->done() || ! $stream->next() instanceof OpeningBracketToken) {
126 return [];
127 }
128
129 $generics = [];
130
131 $stream->forward();
132
133 while (true) {
134 if ($stream->done()) {
135 throw new MissingGenerics($className, $generics, $templates);
136 }
137
138 $generics[] = $stream->read();
139
140 if ($stream->done()) {
141 throw new GenericClosingBracketMissing($className, $generics);
142 }
143
144 $next = $stream->forward();
145
146 if ($next instanceof ClosingBracketToken) {
147 break;
148 }
149
150 if (! $next instanceof CommaToken) {
151 throw new GenericCommaMissing($className, $generics);
152 }
153 }
154
155 return $generics;
156 }
157
158 /**
159 * @param class-string $className
160 * @param array<string, Type> $templates
161 * @param Type[] $generics
162 * @return array<string, Type>
163 */
164 private function assignGenerics(string $className, array $templates, array $generics): array
165 {
166 $assignedGenerics = [];
167
168 foreach ($templates as $name => $template) {
169 $generic = array_shift($generics);
170
171 if ($generic === null) {
172 $remainingTemplates = array_keys(array_slice($templates, count($assignedGenerics)));
173
174 throw new AssignedGenericNotFound($className, ...$remainingTemplates);
175 }
176
177 if ($template instanceof ArrayKeyType && $generic instanceof StringType) {
178 $generic = ArrayKeyType::string();
179 }
180
181 if ($template instanceof ArrayKeyType && $generic instanceof IntegerType) {
182 $generic = ArrayKeyType::integer();
183 }
184
185 if (! $generic->matches($template)) {
186 throw new InvalidAssignedGeneric($generic, $template, $name, $className);
187 }
188
189 $assignedGenerics[$name] = $generic;
190 }
191
192 if (! empty($generics)) {
193 throw new CannotAssignGeneric($className, ...$generics);
194 }
195
196 return $assignedGenerics;
197 }
198
199 /**
200 * @param ReflectionClass<object> $reflection
201 * @param ReflectionClass<object> $parentReflection
202 */
203 private function parentType(ReflectionClass $reflection, ReflectionClass $parentReflection, TypeParser $typeParser): NativeClassType
204 {
205 $extendedClass = DocParser::classExtendsTypes($reflection);
206
207 if (count($extendedClass) > 1) {
208 throw new SeveralExtendTagsFound($reflection);
209 } elseif (count($extendedClass) === 0) {
210 $extendedClass = $parentReflection->name;
211 } else {
212 $extendedClass = $extendedClass[0];
213 }
214
215 try {
216 $parentType = $typeParser->parse($extendedClass);
217 } catch (InvalidType $exception) {
218 throw new ExtendTagTypeError($reflection, $exception);
219 }
220
221 if (! $parentType instanceof NativeClassType) {
222 throw new InvalidExtendTagType($reflection, $parentType);
223 }
224
225 if ($parentType->className() !== $parentReflection->name) {
226 throw new InvalidExtendTagClassName($reflection, $parentType);
227 }
228
229 return $parentType;
230 }
231 }