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: /** @var ConstExprParser|null */
15: private $constExprParser;
16:
17: public function __construct(?ConstExprParser $constExprParser = null)
18: {
19: $this->constExprParser = $constExprParser;
20: }
21:
22: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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(); // because of ConstFetchNode
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(); // because of ConstFetchNode
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(); // because of ConstFetchNode
137: }
138: } else {
139: $tokens->dropSavePoint(); // because of ConstFetchNode
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: // trailing comma case
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: * @phpstan-impure
353: * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*}
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: /** @phpstan-impure */
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: * @phpstan-impure
557: * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
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: