1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\PhpDocParser\Parser; |
4: | |
5: | use LogicException; |
6: | use PHPStan\PhpDocParser\Ast; |
7: | use PHPStan\PhpDocParser\Lexer\Lexer; |
8: | use function strpos; |
9: | use function trim; |
10: | |
11: | class TypeParser |
12: | { |
13: | |
14: | |
15: | private $constExprParser; |
16: | |
17: | public function __construct(?ConstExprParser $constExprParser = null) |
18: | { |
19: | $this->constExprParser = $constExprParser; |
20: | } |
21: | |
22: | |
23: | public function parse(TokenIterator $tokens): Ast\Type\TypeNode |
24: | { |
25: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
26: | $type = $this->parseNullable($tokens); |
27: | |
28: | } else { |
29: | $type = $this->parseAtomic($tokens); |
30: | |
31: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
32: | $type = $this->parseUnion($tokens, $type); |
33: | |
34: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
35: | $type = $this->parseIntersection($tokens, $type); |
36: | } |
37: | } |
38: | |
39: | return $type; |
40: | } |
41: | |
42: | |
43: | private function subParse(TokenIterator $tokens): Ast\Type\TypeNode |
44: | { |
45: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
46: | $type = $this->parseNullable($tokens); |
47: | |
48: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
49: | $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue()); |
50: | |
51: | } else { |
52: | $type = $this->parseAtomic($tokens); |
53: | |
54: | if ($tokens->isCurrentTokenValue('is')) { |
55: | $type = $this->parseConditional($tokens, $type); |
56: | } else { |
57: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
58: | |
59: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
60: | $type = $this->subParseUnion($tokens, $type); |
61: | |
62: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
63: | $type = $this->subParseIntersection($tokens, $type); |
64: | } |
65: | } |
66: | } |
67: | |
68: | return $type; |
69: | } |
70: | |
71: | |
72: | |
73: | private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode |
74: | { |
75: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
76: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
77: | $type = $this->subParse($tokens); |
78: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
79: | |
80: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
81: | |
82: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
83: | return $this->tryParseArrayOrOffsetAccess($tokens, $type); |
84: | } |
85: | |
86: | return $type; |
87: | } |
88: | |
89: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
90: | $type = new Ast\Type\ThisTypeNode(); |
91: | |
92: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
93: | return $this->tryParseArrayOrOffsetAccess($tokens, $type); |
94: | } |
95: | |
96: | return $type; |
97: | } |
98: | |
99: | $currentTokenValue = $tokens->currentTokenValue(); |
100: | $tokens->pushSavePoint(); |
101: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { |
102: | $type = new Ast\Type\IdentifierTypeNode($currentTokenValue); |
103: | |
104: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { |
105: | $tokens->dropSavePoint(); |
106: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { |
107: | $tokens->pushSavePoint(); |
108: | |
109: | $isHtml = $this->isHtml($tokens); |
110: | $tokens->rollback(); |
111: | if ($isHtml) { |
112: | return $type; |
113: | } |
114: | |
115: | $type = $this->parseGeneric($tokens, $type); |
116: | |
117: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
118: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
119: | } |
120: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
121: | $type = $this->tryParseCallable($tokens, $type); |
122: | |
123: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
124: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
125: | |
126: | } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { |
127: | $type = $this->parseArrayShape($tokens, $type); |
128: | |
129: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
130: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
131: | } |
132: | } |
133: | |
134: | return $type; |
135: | } else { |
136: | $tokens->rollback(); |
137: | } |
138: | } else { |
139: | $tokens->dropSavePoint(); |
140: | } |
141: | |
142: | $exception = new ParserException( |
143: | $tokens->currentTokenValue(), |
144: | $tokens->currentTokenType(), |
145: | $tokens->currentTokenOffset(), |
146: | Lexer::TOKEN_IDENTIFIER |
147: | ); |
148: | |
149: | if ($this->constExprParser === null) { |
150: | throw $exception; |
151: | } |
152: | |
153: | try { |
154: | $constExpr = $this->constExprParser->parse($tokens, true); |
155: | if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { |
156: | throw $exception; |
157: | } |
158: | |
159: | return new Ast\Type\ConstTypeNode($constExpr); |
160: | } catch (LogicException $e) { |
161: | throw $exception; |
162: | } |
163: | } |
164: | |
165: | |
166: | |
167: | private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
168: | { |
169: | $types = [$type]; |
170: | |
171: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { |
172: | $types[] = $this->parseAtomic($tokens); |
173: | } |
174: | |
175: | return new Ast\Type\UnionTypeNode($types); |
176: | } |
177: | |
178: | |
179: | |
180: | private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
181: | { |
182: | $types = [$type]; |
183: | |
184: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { |
185: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
186: | $types[] = $this->parseAtomic($tokens); |
187: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
188: | } |
189: | |
190: | return new Ast\Type\UnionTypeNode($types); |
191: | } |
192: | |
193: | |
194: | |
195: | private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
196: | { |
197: | $types = [$type]; |
198: | |
199: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { |
200: | $types[] = $this->parseAtomic($tokens); |
201: | } |
202: | |
203: | return new Ast\Type\IntersectionTypeNode($types); |
204: | } |
205: | |
206: | |
207: | |
208: | private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
209: | { |
210: | $types = [$type]; |
211: | |
212: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { |
213: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
214: | $types[] = $this->parseAtomic($tokens); |
215: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
216: | } |
217: | |
218: | return new Ast\Type\IntersectionTypeNode($types); |
219: | } |
220: | |
221: | |
222: | |
223: | private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode |
224: | { |
225: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
226: | |
227: | $negated = false; |
228: | if ($tokens->isCurrentTokenValue('not')) { |
229: | $negated = true; |
230: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
231: | } |
232: | |
233: | $targetType = $this->parse($tokens); |
234: | |
235: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
236: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
237: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
238: | |
239: | $ifType = $this->parse($tokens); |
240: | |
241: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
242: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
243: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
244: | |
245: | $elseType = $this->subParse($tokens); |
246: | |
247: | return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated); |
248: | } |
249: | |
250: | |
251: | private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode |
252: | { |
253: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
254: | $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is'); |
255: | |
256: | $negated = false; |
257: | if ($tokens->isCurrentTokenValue('not')) { |
258: | $negated = true; |
259: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
260: | } |
261: | |
262: | $targetType = $this->parse($tokens); |
263: | |
264: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
265: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
266: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
267: | |
268: | $ifType = $this->parse($tokens); |
269: | |
270: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
271: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
272: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
273: | |
274: | $elseType = $this->subParse($tokens); |
275: | |
276: | return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated); |
277: | } |
278: | |
279: | |
280: | |
281: | private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode |
282: | { |
283: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
284: | |
285: | $type = $this->parseAtomic($tokens); |
286: | |
287: | return new Ast\Type\NullableTypeNode($type); |
288: | } |
289: | |
290: | |
291: | public function isHtml(TokenIterator $tokens): bool |
292: | { |
293: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
294: | |
295: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { |
296: | return false; |
297: | } |
298: | |
299: | $htmlTagName = $tokens->currentTokenValue(); |
300: | |
301: | $tokens->next(); |
302: | |
303: | if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { |
304: | return false; |
305: | } |
306: | |
307: | while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) { |
308: | if ( |
309: | $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) |
310: | && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false |
311: | ) { |
312: | return true; |
313: | } |
314: | |
315: | $tokens->next(); |
316: | } |
317: | |
318: | return false; |
319: | } |
320: | |
321: | |
322: | public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode |
323: | { |
324: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
325: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
326: | |
327: | $genericTypes = []; |
328: | $variances = []; |
329: | |
330: | [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); |
331: | |
332: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
333: | |
334: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
335: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
336: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { |
337: | |
338: | return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); |
339: | } |
340: | [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); |
341: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
342: | } |
343: | |
344: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
345: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); |
346: | |
347: | return new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); |
348: | } |
349: | |
350: | |
351: | |
352: | |
353: | |
354: | |
355: | public function parseGenericTypeArgument(TokenIterator $tokens): array |
356: | { |
357: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { |
358: | return [ |
359: | new Ast\Type\IdentifierTypeNode('mixed'), |
360: | Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT, |
361: | ]; |
362: | } |
363: | |
364: | if ($tokens->tryConsumeTokenValue('contravariant')) { |
365: | $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT; |
366: | } elseif ($tokens->tryConsumeTokenValue('covariant')) { |
367: | $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT; |
368: | } else { |
369: | $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT; |
370: | } |
371: | |
372: | $type = $this->parse($tokens); |
373: | return [$type, $variance]; |
374: | } |
375: | |
376: | |
377: | |
378: | private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode |
379: | { |
380: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); |
381: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
382: | |
383: | $parameters = []; |
384: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { |
385: | $parameters[] = $this->parseCallableParameter($tokens); |
386: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
387: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
388: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
389: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { |
390: | break; |
391: | } |
392: | $parameters[] = $this->parseCallableParameter($tokens); |
393: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
394: | } |
395: | } |
396: | |
397: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
398: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
399: | $returnType = $this->parseCallableReturnType($tokens); |
400: | |
401: | return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType); |
402: | } |
403: | |
404: | |
405: | |
406: | private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode |
407: | { |
408: | $type = $this->parse($tokens); |
409: | $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); |
410: | $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); |
411: | |
412: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
413: | $parameterName = $tokens->currentTokenValue(); |
414: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
415: | |
416: | } else { |
417: | $parameterName = ''; |
418: | } |
419: | |
420: | $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); |
421: | return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional); |
422: | } |
423: | |
424: | |
425: | |
426: | private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode |
427: | { |
428: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
429: | $type = $this->parseNullable($tokens); |
430: | |
431: | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
432: | $type = $this->parse($tokens); |
433: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
434: | |
435: | } else { |
436: | $type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); |
437: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
438: | |
439: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { |
440: | $type = $this->parseGeneric($tokens, $type); |
441: | |
442: | } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { |
443: | $type = $this->parseArrayShape($tokens, $type); |
444: | } |
445: | } |
446: | |
447: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
448: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
449: | } |
450: | |
451: | return $type; |
452: | } |
453: | |
454: | |
455: | |
456: | private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode |
457: | { |
458: | try { |
459: | $tokens->pushSavePoint(); |
460: | $type = $this->parseCallable($tokens, $identifier); |
461: | $tokens->dropSavePoint(); |
462: | |
463: | } catch (ParserException $e) { |
464: | $tokens->rollback(); |
465: | $type = $identifier; |
466: | } |
467: | |
468: | return $type; |
469: | } |
470: | |
471: | |
472: | |
473: | private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
474: | { |
475: | try { |
476: | while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
477: | $tokens->pushSavePoint(); |
478: | |
479: | $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace(); |
480: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET); |
481: | |
482: | if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) { |
483: | $offset = $this->parse($tokens); |
484: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); |
485: | $tokens->dropSavePoint(); |
486: | $type = new Ast\Type\OffsetAccessTypeNode($type, $offset); |
487: | } else { |
488: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); |
489: | $tokens->dropSavePoint(); |
490: | $type = new Ast\Type\ArrayTypeNode($type); |
491: | } |
492: | } |
493: | |
494: | } catch (ParserException $e) { |
495: | $tokens->rollback(); |
496: | } |
497: | |
498: | return $type; |
499: | } |
500: | |
501: | |
502: | |
503: | private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode |
504: | { |
505: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); |
506: | |
507: | $items = []; |
508: | $sealed = true; |
509: | |
510: | do { |
511: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
512: | |
513: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { |
514: | return new Ast\Type\ArrayShapeNode($items); |
515: | } |
516: | |
517: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { |
518: | $sealed = false; |
519: | $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); |
520: | break; |
521: | } |
522: | |
523: | $items[] = $this->parseArrayShapeItem($tokens); |
524: | |
525: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
526: | } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); |
527: | |
528: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
529: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); |
530: | |
531: | return new Ast\Type\ArrayShapeNode($items, $sealed); |
532: | } |
533: | |
534: | |
535: | |
536: | private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode |
537: | { |
538: | try { |
539: | $tokens->pushSavePoint(); |
540: | $key = $this->parseArrayShapeKey($tokens); |
541: | $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); |
542: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
543: | $value = $this->parse($tokens); |
544: | $tokens->dropSavePoint(); |
545: | |
546: | return new Ast\Type\ArrayShapeItemNode($key, $optional, $value); |
547: | } catch (ParserException $e) { |
548: | $tokens->rollback(); |
549: | $value = $this->parse($tokens); |
550: | |
551: | return new Ast\Type\ArrayShapeItemNode(null, false, $value); |
552: | } |
553: | } |
554: | |
555: | |
556: | |
557: | |
558: | |
559: | private function parseArrayShapeKey(TokenIterator $tokens) |
560: | { |
561: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { |
562: | $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); |
563: | $tokens->next(); |
564: | |
565: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { |
566: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); |
567: | $tokens->next(); |
568: | |
569: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { |
570: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); |
571: | $tokens->next(); |
572: | |
573: | } else { |
574: | $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); |
575: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
576: | } |
577: | |
578: | return $key; |
579: | } |
580: | |
581: | } |
582: | |