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\Ast\PhpDoc\TemplateTagValueNode; |
8: | use PHPStan\PhpDocParser\Lexer\Lexer; |
9: | use function in_array; |
10: | use function str_replace; |
11: | use function strlen; |
12: | use function strpos; |
13: | use function substr_compare; |
14: | use function trim; |
15: | |
16: | class TypeParser |
17: | { |
18: | |
19: | |
20: | private $constExprParser; |
21: | |
22: | |
23: | private $quoteAwareConstExprString; |
24: | |
25: | |
26: | private $useLinesAttributes; |
27: | |
28: | |
29: | private $useIndexAttributes; |
30: | |
31: | |
32: | |
33: | |
34: | public function __construct( |
35: | ?ConstExprParser $constExprParser = null, |
36: | bool $quoteAwareConstExprString = false, |
37: | array $usedAttributes = [] |
38: | ) |
39: | { |
40: | $this->constExprParser = $constExprParser; |
41: | $this->quoteAwareConstExprString = $quoteAwareConstExprString; |
42: | $this->useLinesAttributes = $usedAttributes['lines'] ?? false; |
43: | $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; |
44: | } |
45: | |
46: | |
47: | public function parse(TokenIterator $tokens): Ast\Type\TypeNode |
48: | { |
49: | $startLine = $tokens->currentTokenLine(); |
50: | $startIndex = $tokens->currentTokenIndex(); |
51: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
52: | $type = $this->parseNullable($tokens); |
53: | |
54: | } else { |
55: | $type = $this->parseAtomic($tokens); |
56: | |
57: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
58: | $type = $this->parseUnion($tokens, $type); |
59: | |
60: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
61: | $type = $this->parseIntersection($tokens, $type); |
62: | } |
63: | } |
64: | |
65: | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
66: | } |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | |
74: | public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node |
75: | { |
76: | if ($this->useLinesAttributes) { |
77: | $type->setAttribute(Ast\Attribute::START_LINE, $startLine); |
78: | $type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine()); |
79: | } |
80: | |
81: | if ($this->useIndexAttributes) { |
82: | $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); |
83: | $type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken()); |
84: | } |
85: | |
86: | return $type; |
87: | } |
88: | |
89: | |
90: | private function subParse(TokenIterator $tokens): Ast\Type\TypeNode |
91: | { |
92: | $startLine = $tokens->currentTokenLine(); |
93: | $startIndex = $tokens->currentTokenIndex(); |
94: | |
95: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
96: | $type = $this->parseNullable($tokens); |
97: | |
98: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
99: | $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue()); |
100: | |
101: | } else { |
102: | $type = $this->parseAtomic($tokens); |
103: | |
104: | if ($tokens->isCurrentTokenValue('is')) { |
105: | $type = $this->parseConditional($tokens, $type); |
106: | } else { |
107: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
108: | |
109: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { |
110: | $type = $this->subParseUnion($tokens, $type); |
111: | |
112: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { |
113: | $type = $this->subParseIntersection($tokens, $type); |
114: | } |
115: | } |
116: | } |
117: | |
118: | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
119: | } |
120: | |
121: | |
122: | |
123: | private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode |
124: | { |
125: | $startLine = $tokens->currentTokenLine(); |
126: | $startIndex = $tokens->currentTokenIndex(); |
127: | |
128: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
129: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
130: | $type = $this->subParse($tokens); |
131: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
132: | |
133: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
134: | |
135: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
136: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
137: | } |
138: | |
139: | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
140: | } |
141: | |
142: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
143: | $type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex); |
144: | |
145: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
146: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
147: | } |
148: | |
149: | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
150: | } |
151: | |
152: | $currentTokenValue = $tokens->currentTokenValue(); |
153: | $tokens->pushSavePoint(); |
154: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { |
155: | $type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex); |
156: | |
157: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { |
158: | $tokens->dropSavePoint(); |
159: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { |
160: | $tokens->pushSavePoint(); |
161: | |
162: | $isHtml = $this->isHtml($tokens); |
163: | $tokens->rollback(); |
164: | if ($isHtml) { |
165: | return $type; |
166: | } |
167: | |
168: | $origType = $type; |
169: | $type = $this->tryParseCallable($tokens, $type, true); |
170: | if ($type === $origType) { |
171: | $type = $this->parseGeneric($tokens, $type); |
172: | |
173: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
174: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
175: | } |
176: | } |
177: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
178: | $type = $this->tryParseCallable($tokens, $type, false); |
179: | |
180: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
181: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
182: | |
183: | } elseif (in_array($type->name, [ |
184: | Ast\Type\ArrayShapeNode::KIND_ARRAY, |
185: | Ast\Type\ArrayShapeNode::KIND_LIST, |
186: | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, |
187: | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, |
188: | 'object', |
189: | ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { |
190: | if ($type->name === 'object') { |
191: | $type = $this->parseObjectShape($tokens); |
192: | } else { |
193: | $type = $this->parseArrayShape($tokens, $type, $type->name); |
194: | } |
195: | |
196: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
197: | $type = $this->tryParseArrayOrOffsetAccess( |
198: | $tokens, |
199: | $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex) |
200: | ); |
201: | } |
202: | } |
203: | |
204: | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
205: | } else { |
206: | $tokens->rollback(); |
207: | } |
208: | } else { |
209: | $tokens->dropSavePoint(); |
210: | } |
211: | |
212: | $currentTokenValue = $tokens->currentTokenValue(); |
213: | $currentTokenType = $tokens->currentTokenType(); |
214: | $currentTokenOffset = $tokens->currentTokenOffset(); |
215: | $currentTokenLine = $tokens->currentTokenLine(); |
216: | |
217: | if ($this->constExprParser === null) { |
218: | throw new ParserException( |
219: | $currentTokenValue, |
220: | $currentTokenType, |
221: | $currentTokenOffset, |
222: | Lexer::TOKEN_IDENTIFIER, |
223: | null, |
224: | $currentTokenLine |
225: | ); |
226: | } |
227: | |
228: | try { |
229: | $constExpr = $this->constExprParser->parse($tokens, true); |
230: | if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { |
231: | throw new ParserException( |
232: | $currentTokenValue, |
233: | $currentTokenType, |
234: | $currentTokenOffset, |
235: | Lexer::TOKEN_IDENTIFIER, |
236: | null, |
237: | $currentTokenLine |
238: | ); |
239: | } |
240: | |
241: | $type = $this->enrichWithAttributes( |
242: | $tokens, |
243: | new Ast\Type\ConstTypeNode($constExpr), |
244: | $startLine, |
245: | $startIndex |
246: | ); |
247: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
248: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
249: | } |
250: | |
251: | return $type; |
252: | } catch (LogicException $e) { |
253: | throw new ParserException( |
254: | $currentTokenValue, |
255: | $currentTokenType, |
256: | $currentTokenOffset, |
257: | Lexer::TOKEN_IDENTIFIER, |
258: | null, |
259: | $currentTokenLine |
260: | ); |
261: | } |
262: | } |
263: | |
264: | |
265: | |
266: | private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
267: | { |
268: | $types = [$type]; |
269: | |
270: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { |
271: | $types[] = $this->parseAtomic($tokens); |
272: | } |
273: | |
274: | return new Ast\Type\UnionTypeNode($types); |
275: | } |
276: | |
277: | |
278: | |
279: | private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
280: | { |
281: | $types = [$type]; |
282: | |
283: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { |
284: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
285: | $types[] = $this->parseAtomic($tokens); |
286: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
287: | } |
288: | |
289: | return new Ast\Type\UnionTypeNode($types); |
290: | } |
291: | |
292: | |
293: | |
294: | private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
295: | { |
296: | $types = [$type]; |
297: | |
298: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { |
299: | $types[] = $this->parseAtomic($tokens); |
300: | } |
301: | |
302: | return new Ast\Type\IntersectionTypeNode($types); |
303: | } |
304: | |
305: | |
306: | |
307: | private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
308: | { |
309: | $types = [$type]; |
310: | |
311: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { |
312: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
313: | $types[] = $this->parseAtomic($tokens); |
314: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
315: | } |
316: | |
317: | return new Ast\Type\IntersectionTypeNode($types); |
318: | } |
319: | |
320: | |
321: | |
322: | private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode |
323: | { |
324: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
325: | |
326: | $negated = false; |
327: | if ($tokens->isCurrentTokenValue('not')) { |
328: | $negated = true; |
329: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
330: | } |
331: | |
332: | $targetType = $this->parse($tokens); |
333: | |
334: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
335: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
336: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
337: | |
338: | $ifType = $this->parse($tokens); |
339: | |
340: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
341: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
342: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
343: | |
344: | $elseType = $this->subParse($tokens); |
345: | |
346: | return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated); |
347: | } |
348: | |
349: | |
350: | private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode |
351: | { |
352: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
353: | $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is'); |
354: | |
355: | $negated = false; |
356: | if ($tokens->isCurrentTokenValue('not')) { |
357: | $negated = true; |
358: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
359: | } |
360: | |
361: | $targetType = $this->parse($tokens); |
362: | |
363: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
364: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
365: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
366: | |
367: | $ifType = $this->parse($tokens); |
368: | |
369: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
370: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
371: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
372: | |
373: | $elseType = $this->subParse($tokens); |
374: | |
375: | return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated); |
376: | } |
377: | |
378: | |
379: | |
380: | private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode |
381: | { |
382: | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); |
383: | |
384: | $type = $this->parseAtomic($tokens); |
385: | |
386: | return new Ast\Type\NullableTypeNode($type); |
387: | } |
388: | |
389: | |
390: | public function isHtml(TokenIterator $tokens): bool |
391: | { |
392: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
393: | |
394: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { |
395: | return false; |
396: | } |
397: | |
398: | $htmlTagName = $tokens->currentTokenValue(); |
399: | |
400: | $tokens->next(); |
401: | |
402: | if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { |
403: | return false; |
404: | } |
405: | |
406: | $endTag = '</' . $htmlTagName . '>'; |
407: | $endTagSearchOffset = - strlen($endTag); |
408: | |
409: | while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) { |
410: | if ( |
411: | ( |
412: | $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) |
413: | && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false |
414: | ) |
415: | || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0 |
416: | ) { |
417: | return true; |
418: | } |
419: | |
420: | $tokens->next(); |
421: | } |
422: | |
423: | return false; |
424: | } |
425: | |
426: | |
427: | public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode |
428: | { |
429: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
430: | |
431: | $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); |
432: | $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); |
433: | $genericTypes = []; |
434: | $variances = []; |
435: | |
436: | $isFirst = true; |
437: | while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
438: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
439: | |
440: | |
441: | if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { |
442: | break; |
443: | } |
444: | $isFirst = false; |
445: | |
446: | [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); |
447: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
448: | } |
449: | |
450: | $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); |
451: | if ($startLine !== null && $startIndex !== null) { |
452: | $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); |
453: | } |
454: | |
455: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); |
456: | |
457: | return $type; |
458: | } |
459: | |
460: | |
461: | |
462: | |
463: | |
464: | |
465: | public function parseGenericTypeArgument(TokenIterator $tokens): array |
466: | { |
467: | $startLine = $tokens->currentTokenLine(); |
468: | $startIndex = $tokens->currentTokenIndex(); |
469: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { |
470: | return [ |
471: | $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex), |
472: | Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT, |
473: | ]; |
474: | } |
475: | |
476: | if ($tokens->tryConsumeTokenValue('contravariant')) { |
477: | $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT; |
478: | } elseif ($tokens->tryConsumeTokenValue('covariant')) { |
479: | $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT; |
480: | } else { |
481: | $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT; |
482: | } |
483: | |
484: | $type = $this->parse($tokens); |
485: | return [$type, $variance]; |
486: | } |
487: | |
488: | |
489: | |
490: | |
491: | |
492: | public function parseTemplateTagValue( |
493: | TokenIterator $tokens, |
494: | ?callable $parseDescription = null |
495: | ): TemplateTagValueNode |
496: | { |
497: | $name = $tokens->currentTokenValue(); |
498: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
499: | |
500: | $upperBound = $lowerBound = null; |
501: | |
502: | if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { |
503: | $upperBound = $this->parse($tokens); |
504: | } |
505: | |
506: | if ($tokens->tryConsumeTokenValue('super')) { |
507: | $lowerBound = $this->parse($tokens); |
508: | } |
509: | |
510: | if ($tokens->tryConsumeTokenValue('=')) { |
511: | $default = $this->parse($tokens); |
512: | } else { |
513: | $default = null; |
514: | } |
515: | |
516: | if ($parseDescription !== null) { |
517: | $description = $parseDescription($tokens); |
518: | } else { |
519: | $description = ''; |
520: | } |
521: | |
522: | if ($name === '') { |
523: | throw new LogicException('Template tag name cannot be empty.'); |
524: | } |
525: | |
526: | return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound); |
527: | } |
528: | |
529: | |
530: | |
531: | private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode |
532: | { |
533: | $templates = $hasTemplate |
534: | ? $this->parseCallableTemplates($tokens) |
535: | : []; |
536: | |
537: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); |
538: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
539: | |
540: | $parameters = []; |
541: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { |
542: | $parameters[] = $this->parseCallableParameter($tokens); |
543: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
544: | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
545: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
546: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { |
547: | break; |
548: | } |
549: | $parameters[] = $this->parseCallableParameter($tokens); |
550: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
551: | } |
552: | } |
553: | |
554: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
555: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
556: | |
557: | $startLine = $tokens->currentTokenLine(); |
558: | $startIndex = $tokens->currentTokenIndex(); |
559: | $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex); |
560: | |
561: | return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates); |
562: | } |
563: | |
564: | |
565: | |
566: | |
567: | |
568: | |
569: | |
570: | private function parseCallableTemplates(TokenIterator $tokens): array |
571: | { |
572: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
573: | |
574: | $templates = []; |
575: | |
576: | $isFirst = true; |
577: | while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
578: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
579: | |
580: | |
581: | if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { |
582: | break; |
583: | } |
584: | $isFirst = false; |
585: | |
586: | $templates[] = $this->parseCallableTemplateArgument($tokens); |
587: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
588: | } |
589: | |
590: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); |
591: | |
592: | return $templates; |
593: | } |
594: | |
595: | |
596: | private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode |
597: | { |
598: | $startLine = $tokens->currentTokenLine(); |
599: | $startIndex = $tokens->currentTokenIndex(); |
600: | |
601: | return $this->enrichWithAttributes( |
602: | $tokens, |
603: | $this->parseTemplateTagValue($tokens), |
604: | $startLine, |
605: | $startIndex |
606: | ); |
607: | } |
608: | |
609: | |
610: | |
611: | private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode |
612: | { |
613: | $startLine = $tokens->currentTokenLine(); |
614: | $startIndex = $tokens->currentTokenIndex(); |
615: | $type = $this->parse($tokens); |
616: | $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); |
617: | $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); |
618: | |
619: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { |
620: | $parameterName = $tokens->currentTokenValue(); |
621: | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); |
622: | |
623: | } else { |
624: | $parameterName = ''; |
625: | } |
626: | |
627: | $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); |
628: | return $this->enrichWithAttributes( |
629: | $tokens, |
630: | new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional), |
631: | $startLine, |
632: | $startIndex |
633: | ); |
634: | } |
635: | |
636: | |
637: | |
638: | private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode |
639: | { |
640: | $startLine = $tokens->currentTokenLine(); |
641: | $startIndex = $tokens->currentTokenIndex(); |
642: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { |
643: | return $this->parseNullable($tokens); |
644: | |
645: | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { |
646: | $type = $this->subParse($tokens); |
647: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); |
648: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
649: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
650: | } |
651: | |
652: | return $type; |
653: | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { |
654: | $type = new Ast\Type\ThisTypeNode(); |
655: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
656: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( |
657: | $tokens, |
658: | $type, |
659: | $startLine, |
660: | $startIndex |
661: | )); |
662: | } |
663: | |
664: | return $type; |
665: | } else { |
666: | $currentTokenValue = $tokens->currentTokenValue(); |
667: | $tokens->pushSavePoint(); |
668: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { |
669: | $type = new Ast\Type\IdentifierTypeNode($currentTokenValue); |
670: | |
671: | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { |
672: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { |
673: | $type = $this->parseGeneric( |
674: | $tokens, |
675: | $this->enrichWithAttributes( |
676: | $tokens, |
677: | $type, |
678: | $startLine, |
679: | $startIndex |
680: | ) |
681: | ); |
682: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
683: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( |
684: | $tokens, |
685: | $type, |
686: | $startLine, |
687: | $startIndex |
688: | )); |
689: | } |
690: | |
691: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
692: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( |
693: | $tokens, |
694: | $type, |
695: | $startLine, |
696: | $startIndex |
697: | )); |
698: | |
699: | } elseif (in_array($type->name, [ |
700: | Ast\Type\ArrayShapeNode::KIND_ARRAY, |
701: | Ast\Type\ArrayShapeNode::KIND_LIST, |
702: | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, |
703: | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, |
704: | 'object', |
705: | ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { |
706: | if ($type->name === 'object') { |
707: | $type = $this->parseObjectShape($tokens); |
708: | } else { |
709: | $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes( |
710: | $tokens, |
711: | $type, |
712: | $startLine, |
713: | $startIndex |
714: | ), $type->name); |
715: | } |
716: | |
717: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
718: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( |
719: | $tokens, |
720: | $type, |
721: | $startLine, |
722: | $startIndex |
723: | )); |
724: | } |
725: | } |
726: | |
727: | return $type; |
728: | } else { |
729: | $tokens->rollback(); |
730: | } |
731: | } else { |
732: | $tokens->dropSavePoint(); |
733: | } |
734: | } |
735: | |
736: | $currentTokenValue = $tokens->currentTokenValue(); |
737: | $currentTokenType = $tokens->currentTokenType(); |
738: | $currentTokenOffset = $tokens->currentTokenOffset(); |
739: | $currentTokenLine = $tokens->currentTokenLine(); |
740: | |
741: | if ($this->constExprParser === null) { |
742: | throw new ParserException( |
743: | $currentTokenValue, |
744: | $currentTokenType, |
745: | $currentTokenOffset, |
746: | Lexer::TOKEN_IDENTIFIER, |
747: | null, |
748: | $currentTokenLine |
749: | ); |
750: | } |
751: | |
752: | try { |
753: | $constExpr = $this->constExprParser->parse($tokens, true); |
754: | if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { |
755: | throw new ParserException( |
756: | $currentTokenValue, |
757: | $currentTokenType, |
758: | $currentTokenOffset, |
759: | Lexer::TOKEN_IDENTIFIER, |
760: | null, |
761: | $currentTokenLine |
762: | ); |
763: | } |
764: | |
765: | $type = $this->enrichWithAttributes( |
766: | $tokens, |
767: | new Ast\Type\ConstTypeNode($constExpr), |
768: | $startLine, |
769: | $startIndex |
770: | ); |
771: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
772: | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); |
773: | } |
774: | |
775: | return $type; |
776: | } catch (LogicException $e) { |
777: | throw new ParserException( |
778: | $currentTokenValue, |
779: | $currentTokenType, |
780: | $currentTokenOffset, |
781: | Lexer::TOKEN_IDENTIFIER, |
782: | null, |
783: | $currentTokenLine |
784: | ); |
785: | } |
786: | } |
787: | |
788: | |
789: | |
790: | private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode |
791: | { |
792: | try { |
793: | $tokens->pushSavePoint(); |
794: | $type = $this->parseCallable($tokens, $identifier, $hasTemplate); |
795: | $tokens->dropSavePoint(); |
796: | |
797: | } catch (ParserException $e) { |
798: | $tokens->rollback(); |
799: | $type = $identifier; |
800: | } |
801: | |
802: | return $type; |
803: | } |
804: | |
805: | |
806: | |
807: | private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode |
808: | { |
809: | $startLine = $type->getAttribute(Ast\Attribute::START_LINE); |
810: | $startIndex = $type->getAttribute(Ast\Attribute::START_INDEX); |
811: | try { |
812: | while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { |
813: | $tokens->pushSavePoint(); |
814: | |
815: | $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace(); |
816: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET); |
817: | |
818: | if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) { |
819: | $offset = $this->parse($tokens); |
820: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); |
821: | $tokens->dropSavePoint(); |
822: | $type = new Ast\Type\OffsetAccessTypeNode($type, $offset); |
823: | |
824: | if ($startLine !== null && $startIndex !== null) { |
825: | $type = $this->enrichWithAttributes( |
826: | $tokens, |
827: | $type, |
828: | $startLine, |
829: | $startIndex |
830: | ); |
831: | } |
832: | } else { |
833: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); |
834: | $tokens->dropSavePoint(); |
835: | $type = new Ast\Type\ArrayTypeNode($type); |
836: | |
837: | if ($startLine !== null && $startIndex !== null) { |
838: | $type = $this->enrichWithAttributes( |
839: | $tokens, |
840: | $type, |
841: | $startLine, |
842: | $startIndex |
843: | ); |
844: | } |
845: | } |
846: | } |
847: | |
848: | } catch (ParserException $e) { |
849: | $tokens->rollback(); |
850: | } |
851: | |
852: | return $type; |
853: | } |
854: | |
855: | |
856: | |
857: | |
858: | |
859: | |
860: | private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode |
861: | { |
862: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); |
863: | |
864: | $items = []; |
865: | $sealed = true; |
866: | $unsealedType = null; |
867: | |
868: | do { |
869: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
870: | |
871: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { |
872: | return new Ast\Type\ArrayShapeNode($items, true, $kind); |
873: | } |
874: | |
875: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { |
876: | $sealed = false; |
877: | |
878: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
879: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { |
880: | if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { |
881: | $unsealedType = $this->parseArrayShapeUnsealedType($tokens); |
882: | } else { |
883: | $unsealedType = $this->parseListShapeUnsealedType($tokens); |
884: | } |
885: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
886: | } |
887: | |
888: | $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); |
889: | break; |
890: | } |
891: | |
892: | $items[] = $this->parseArrayShapeItem($tokens); |
893: | |
894: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
895: | } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); |
896: | |
897: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
898: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); |
899: | |
900: | return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $unsealedType); |
901: | } |
902: | |
903: | |
904: | |
905: | private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode |
906: | { |
907: | $startLine = $tokens->currentTokenLine(); |
908: | $startIndex = $tokens->currentTokenIndex(); |
909: | try { |
910: | $tokens->pushSavePoint(); |
911: | $key = $this->parseArrayShapeKey($tokens); |
912: | $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); |
913: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
914: | $value = $this->parse($tokens); |
915: | $tokens->dropSavePoint(); |
916: | |
917: | return $this->enrichWithAttributes( |
918: | $tokens, |
919: | new Ast\Type\ArrayShapeItemNode($key, $optional, $value), |
920: | $startLine, |
921: | $startIndex |
922: | ); |
923: | } catch (ParserException $e) { |
924: | $tokens->rollback(); |
925: | $value = $this->parse($tokens); |
926: | |
927: | return $this->enrichWithAttributes( |
928: | $tokens, |
929: | new Ast\Type\ArrayShapeItemNode(null, false, $value), |
930: | $startLine, |
931: | $startIndex |
932: | ); |
933: | } |
934: | } |
935: | |
936: | |
937: | |
938: | |
939: | |
940: | private function parseArrayShapeKey(TokenIterator $tokens) |
941: | { |
942: | $startIndex = $tokens->currentTokenIndex(); |
943: | $startLine = $tokens->currentTokenLine(); |
944: | |
945: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { |
946: | $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue())); |
947: | $tokens->next(); |
948: | |
949: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { |
950: | if ($this->quoteAwareConstExprString) { |
951: | $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); |
952: | } else { |
953: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); |
954: | } |
955: | $tokens->next(); |
956: | |
957: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { |
958: | if ($this->quoteAwareConstExprString) { |
959: | $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); |
960: | } else { |
961: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); |
962: | } |
963: | |
964: | $tokens->next(); |
965: | |
966: | } else { |
967: | $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); |
968: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
969: | } |
970: | |
971: | return $this->enrichWithAttributes( |
972: | $tokens, |
973: | $key, |
974: | $startLine, |
975: | $startIndex |
976: | ); |
977: | } |
978: | |
979: | |
980: | |
981: | |
982: | private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode |
983: | { |
984: | $startLine = $tokens->currentTokenLine(); |
985: | $startIndex = $tokens->currentTokenIndex(); |
986: | |
987: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
988: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
989: | |
990: | $valueType = $this->parse($tokens); |
991: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
992: | |
993: | $keyType = null; |
994: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { |
995: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
996: | |
997: | $keyType = $valueType; |
998: | $valueType = $this->parse($tokens); |
999: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1000: | } |
1001: | |
1002: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); |
1003: | |
1004: | return $this->enrichWithAttributes( |
1005: | $tokens, |
1006: | new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType), |
1007: | $startLine, |
1008: | $startIndex |
1009: | ); |
1010: | } |
1011: | |
1012: | |
1013: | |
1014: | |
1015: | private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode |
1016: | { |
1017: | $startLine = $tokens->currentTokenLine(); |
1018: | $startIndex = $tokens->currentTokenIndex(); |
1019: | |
1020: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); |
1021: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1022: | |
1023: | $valueType = $this->parse($tokens); |
1024: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1025: | |
1026: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); |
1027: | |
1028: | return $this->enrichWithAttributes( |
1029: | $tokens, |
1030: | new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null), |
1031: | $startLine, |
1032: | $startIndex |
1033: | ); |
1034: | } |
1035: | |
1036: | |
1037: | |
1038: | |
1039: | private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode |
1040: | { |
1041: | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); |
1042: | |
1043: | $items = []; |
1044: | |
1045: | do { |
1046: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1047: | |
1048: | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { |
1049: | return new Ast\Type\ObjectShapeNode($items); |
1050: | } |
1051: | |
1052: | $items[] = $this->parseObjectShapeItem($tokens); |
1053: | |
1054: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1055: | } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); |
1056: | |
1057: | $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); |
1058: | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); |
1059: | |
1060: | return new Ast\Type\ObjectShapeNode($items); |
1061: | } |
1062: | |
1063: | |
1064: | private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode |
1065: | { |
1066: | $startLine = $tokens->currentTokenLine(); |
1067: | $startIndex = $tokens->currentTokenIndex(); |
1068: | |
1069: | $key = $this->parseObjectShapeKey($tokens); |
1070: | $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); |
1071: | $tokens->consumeTokenType(Lexer::TOKEN_COLON); |
1072: | $value = $this->parse($tokens); |
1073: | |
1074: | return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex); |
1075: | } |
1076: | |
1077: | |
1078: | |
1079: | |
1080: | |
1081: | private function parseObjectShapeKey(TokenIterator $tokens) |
1082: | { |
1083: | $startLine = $tokens->currentTokenLine(); |
1084: | $startIndex = $tokens->currentTokenIndex(); |
1085: | |
1086: | if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { |
1087: | if ($this->quoteAwareConstExprString) { |
1088: | $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED); |
1089: | } else { |
1090: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'")); |
1091: | } |
1092: | $tokens->next(); |
1093: | |
1094: | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { |
1095: | if ($this->quoteAwareConstExprString) { |
1096: | $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED); |
1097: | } else { |
1098: | $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"')); |
1099: | } |
1100: | $tokens->next(); |
1101: | |
1102: | } else { |
1103: | $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); |
1104: | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); |
1105: | } |
1106: | |
1107: | return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex); |
1108: | } |
1109: | |
1110: | } |
1111: | |