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