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: $endTag = '</' . $htmlTagName . '>';
423: $endTagSearchOffset = - strlen($endTag);
424:
425: while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
426: if (
427: (
428: $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
429: && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
430: )
431: || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0
432: ) {
433: return true;
434: }
435:
436: $tokens->next();
437: }
438:
439: return false;
440: }
441:
442: /** @phpstan-impure */
443: public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
444: {
445: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
446: $tokens->skipNewLineTokensAndConsumeComments();
447:
448: $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
449: $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
450: $genericTypes = [];
451: $variances = [];
452:
453: $isFirst = true;
454: while (
455: $isFirst
456: || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)
457: ) {
458: $tokens->skipNewLineTokensAndConsumeComments();
459:
460: // trailing comma case
461: if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
462: break;
463: }
464: $isFirst = false;
465:
466: [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens);
467: $tokens->skipNewLineTokensAndConsumeComments();
468: }
469:
470: $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
471: if ($startLine !== null && $startIndex !== null) {
472: $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
473: } else {
474: // The comments read between the arguments have to be given away
475: // even where the node is not placed, or they would still be waiting
476: // for a node once the whole PHPDoc has been read
477: $tokens->flushComments();
478: }
479:
480: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
481:
482: return $type;
483: }
484:
485: /**
486: * @phpstan-impure
487: * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*}
488: */
489: public function parseGenericTypeArgument(TokenIterator $tokens): array
490: {
491: $startLine = $tokens->currentTokenLine();
492: $startIndex = $tokens->currentTokenIndex();
493: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
494: return [
495: $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex),
496: Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
497: ];
498: }
499:
500: if ($tokens->tryConsumeTokenValue('contravariant')) {
501: $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT;
502: } elseif ($tokens->tryConsumeTokenValue('covariant')) {
503: $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT;
504: } else {
505: $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT;
506: }
507:
508: $type = $this->parse($tokens);
509: return [$type, $variance];
510: }
511:
512: /**
513: * @throws ParserException
514: * @param ?callable(TokenIterator): string $parseDescription
515: */
516: public function parseTemplateTagValue(
517: TokenIterator $tokens,
518: ?callable $parseDescription = null
519: ): TemplateTagValueNode
520: {
521: $name = $tokens->currentTokenValue();
522: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
523:
524: $upperBound = $lowerBound = null;
525:
526: if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
527: $upperBound = $this->parse($tokens);
528: }
529:
530: if ($tokens->tryConsumeTokenValue('super')) {
531: $lowerBound = $this->parse($tokens);
532: }
533:
534: if ($tokens->tryConsumeTokenValue('=')) {
535: $default = $this->parse($tokens);
536: } else {
537: $default = null;
538: }
539:
540: if ($parseDescription !== null) {
541: $description = $parseDescription($tokens);
542: } else {
543: $description = '';
544: }
545:
546: if ($name === '') {
547: throw new LogicException('Template tag name cannot be empty.');
548: }
549:
550: return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
551: }
552:
553: /** @phpstan-impure */
554: private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
555: {
556: $templates = $hasTemplate
557: ? $this->parseCallableTemplates($tokens)
558: : [];
559:
560: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
561: $tokens->skipNewLineTokensAndConsumeComments();
562:
563: $parameters = [];
564: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
565: $parameters[] = $this->parseCallableParameter($tokens);
566: $tokens->skipNewLineTokensAndConsumeComments();
567: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
568: $tokens->skipNewLineTokensAndConsumeComments();
569: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
570: break;
571: }
572: $parameters[] = $this->parseCallableParameter($tokens);
573: $tokens->skipNewLineTokensAndConsumeComments();
574: }
575: }
576:
577: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
578: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
579:
580: $startLine = $tokens->currentTokenLine();
581: $startIndex = $tokens->currentTokenIndex();
582: $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
583:
584: return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
585: }
586:
587: /**
588: * @return Ast\PhpDoc\TemplateTagValueNode[]
589: *
590: * @phpstan-impure
591: */
592: private function parseCallableTemplates(TokenIterator $tokens): array
593: {
594: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
595:
596: $templates = [];
597:
598: $isFirst = true;
599: while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
600: $tokens->skipNewLineTokensAndConsumeComments();
601:
602: // trailing comma case
603: if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
604: break;
605: }
606: $isFirst = false;
607:
608: $templates[] = $this->parseCallableTemplateArgument($tokens);
609: $tokens->skipNewLineTokensAndConsumeComments();
610: }
611:
612: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
613:
614: return $templates;
615: }
616:
617: private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode
618: {
619: $startLine = $tokens->currentTokenLine();
620: $startIndex = $tokens->currentTokenIndex();
621:
622: return $this->enrichWithAttributes(
623: $tokens,
624: $this->parseTemplateTagValue($tokens),
625: $startLine,
626: $startIndex,
627: );
628: }
629:
630: /** @phpstan-impure */
631: private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
632: {
633: $startLine = $tokens->currentTokenLine();
634: $startIndex = $tokens->currentTokenIndex();
635: $type = $this->parse($tokens);
636: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
637: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
638:
639: if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
640: $parameterName = $tokens->currentTokenValue();
641: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
642:
643: } else {
644: $parameterName = '';
645: }
646:
647: $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
648: return $this->enrichWithAttributes(
649: $tokens,
650: new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional),
651: $startLine,
652: $startIndex,
653: );
654: }
655:
656: /** @phpstan-impure */
657: private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
658: {
659: $startLine = $tokens->currentTokenLine();
660: $startIndex = $tokens->currentTokenIndex();
661: if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
662: return $this->parseNullable($tokens);
663:
664: } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
665: $type = $this->subParse($tokens);
666: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
667: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
668: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
669: }
670:
671: return $type;
672: } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
673: $type = new Ast\Type\ThisTypeNode();
674: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
675: $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
676: $tokens,
677: $type,
678: $startLine,
679: $startIndex,
680: ));
681: }
682:
683: return $type;
684: } else {
685: $currentTokenValue = $tokens->currentTokenValue();
686: $tokens->pushSavePoint(); // because of ConstFetchNode
687: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
688: $type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
689:
690: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
691: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
692: $type = $this->parseGeneric(
693: $tokens,
694: $this->enrichWithAttributes(
695: $tokens,
696: $type,
697: $startLine,
698: $startIndex,
699: ),
700: );
701: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
702: $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
703: $tokens,
704: $type,
705: $startLine,
706: $startIndex,
707: ));
708: }
709:
710: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
711: $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
712: $tokens,
713: $type,
714: $startLine,
715: $startIndex,
716: ));
717:
718: } elseif (in_array($type->name, [
719: Ast\Type\ArrayShapeNode::KIND_ARRAY,
720: Ast\Type\ArrayShapeNode::KIND_LIST,
721: Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY,
722: Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST,
723: 'object',
724: ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
725: if ($type->name === 'object') {
726: $type = $this->parseObjectShape($tokens);
727: } else {
728: $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes(
729: $tokens,
730: $type,
731: $startLine,
732: $startIndex,
733: ), $type->name);
734: }
735:
736: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
737: $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes(
738: $tokens,
739: $type,
740: $startLine,
741: $startIndex,
742: ));
743: }
744: }
745:
746: return $type;
747: } else {
748: $tokens->rollback(); // because of ConstFetchNode
749: }
750: } else {
751: $tokens->dropSavePoint(); // because of ConstFetchNode
752: }
753: }
754:
755: $currentTokenValue = $tokens->currentTokenValue();
756: $currentTokenType = $tokens->currentTokenType();
757: $currentTokenOffset = $tokens->currentTokenOffset();
758: $currentTokenLine = $tokens->currentTokenLine();
759:
760: try {
761: $constExpr = $this->constExprParser->parse($tokens);
762: if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
763: throw new ParserException(
764: $currentTokenValue,
765: $currentTokenType,
766: $currentTokenOffset,
767: Lexer::TOKEN_IDENTIFIER,
768: null,
769: $currentTokenLine,
770: );
771: }
772:
773: $type = $this->enrichWithAttributes(
774: $tokens,
775: new Ast\Type\ConstTypeNode($constExpr),
776: $startLine,
777: $startIndex,
778: );
779: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
780: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
781: }
782:
783: return $type;
784: } catch (LogicException $e) {
785: throw new ParserException(
786: $currentTokenValue,
787: $currentTokenType,
788: $currentTokenOffset,
789: Lexer::TOKEN_IDENTIFIER,
790: null,
791: $currentTokenLine,
792: );
793: }
794: }
795:
796: /** @phpstan-impure */
797: private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode
798: {
799: try {
800: $tokens->pushSavePoint();
801: $type = $this->parseCallable($tokens, $identifier, $hasTemplate);
802: $tokens->dropSavePoint();
803:
804: } catch (ParserException $e) {
805: $tokens->rollback();
806: $type = $identifier;
807: }
808:
809: return $type;
810: }
811:
812: /** @phpstan-impure */
813: private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
814: {
815: $startLine = $type->getAttribute(Ast\Attribute::START_LINE);
816: $startIndex = $type->getAttribute(Ast\Attribute::START_INDEX);
817: try {
818: while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
819: $tokens->pushSavePoint();
820:
821: $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace();
822: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET);
823:
824: if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) {
825: $offset = $this->parse($tokens);
826: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
827: $tokens->dropSavePoint();
828: $type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
829:
830: if ($startLine !== null && $startIndex !== null) {
831: $type = $this->enrichWithAttributes(
832: $tokens,
833: $type,
834: $startLine,
835: $startIndex,
836: );
837: }
838: } else {
839: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
840: $tokens->dropSavePoint();
841: $type = new Ast\Type\ArrayTypeNode($type);
842:
843: if ($startLine !== null && $startIndex !== null) {
844: $type = $this->enrichWithAttributes(
845: $tokens,
846: $type,
847: $startLine,
848: $startIndex,
849: );
850: }
851: }
852: }
853:
854: } catch (ParserException $e) {
855: $tokens->rollback();
856: }
857:
858: return $type;
859: }
860:
861: /**
862: * @phpstan-impure
863: * @param Ast\Type\ArrayShapeNode::KIND_* $kind
864: */
865: private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode
866: {
867: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
868:
869: $items = [];
870: $sealed = true;
871: $unsealedType = null;
872:
873: $done = false;
874:
875: do {
876: $tokens->skipNewLineTokensAndConsumeComments();
877:
878: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
879: return Ast\Type\ArrayShapeNode::createSealed($items, $kind);
880: }
881:
882: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
883: $sealed = false;
884:
885: $tokens->skipNewLineTokensAndConsumeComments();
886: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
887: if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) {
888: $unsealedType = $this->parseArrayShapeUnsealedType($tokens);
889: } else {
890: $unsealedType = $this->parseListShapeUnsealedType($tokens);
891: }
892: $tokens->skipNewLineTokensAndConsumeComments();
893: }
894:
895: $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
896: break;
897: }
898:
899: $items[] = $this->parseArrayShapeItem($tokens);
900: $tokens->skipNewLineTokensAndConsumeComments();
901: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
902: $done = true;
903: }
904: if ($tokens->currentTokenType() !== Lexer::TOKEN_COMMENT) {
905: continue;
906: }
907:
908: $tokens->next();
909:
910: } while (!$done);
911:
912: $tokens->skipNewLineTokensAndConsumeComments();
913: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
914:
915: if ($sealed) {
916: return Ast\Type\ArrayShapeNode::createSealed($items, $kind);
917: }
918:
919: return Ast\Type\ArrayShapeNode::createUnsealed($items, $unsealedType, $kind);
920: }
921:
922: /** @phpstan-impure */
923: private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
924: {
925: $startLine = $tokens->currentTokenLine();
926: $startIndex = $tokens->currentTokenIndex();
927:
928: // parse any comments above the item
929: $tokens->skipNewLineTokensAndConsumeComments();
930:
931: try {
932: $tokens->pushSavePoint();
933: $key = $this->parseArrayShapeKey($tokens);
934: $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
935: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
936: $value = $this->parse($tokens);
937:
938: $tokens->dropSavePoint();
939:
940: return $this->enrichWithAttributes(
941: $tokens,
942: new Ast\Type\ArrayShapeItemNode($key, $optional, $value),
943: $startLine,
944: $startIndex,
945: );
946: } catch (ParserException $e) {
947: $tokens->rollback();
948: $value = $this->parse($tokens);
949:
950: return $this->enrichWithAttributes(
951: $tokens,
952: new Ast\Type\ArrayShapeItemNode(null, false, $value),
953: $startLine,
954: $startIndex,
955: );
956: }
957: }
958:
959: /**
960: * @phpstan-impure
961: * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\ConstExpr\ConstFetchNode|Ast\Type\IdentifierTypeNode
962: */
963: private function parseArrayShapeKey(TokenIterator $tokens)
964: {
965: $startIndex = $tokens->currentTokenIndex();
966: $startLine = $tokens->currentTokenLine();
967:
968: if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
969: $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
970: $tokens->next();
971:
972: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
973: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
974: $tokens->next();
975:
976: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
977: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED);
978:
979: $tokens->next();
980:
981: } else {
982: $identifier = $tokens->currentTokenValue();
983: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
984:
985: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
986: $classConstantName = $tokens->currentTokenValue();
987: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
988:
989: $key = new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName);
990: } else {
991: $key = new Ast\Type\IdentifierTypeNode($identifier);
992: }
993: }
994:
995: return $this->enrichWithAttributes(
996: $tokens,
997: $key,
998: $startLine,
999: $startIndex,
1000: );
1001: }
1002:
1003: /**
1004: * @phpstan-impure
1005: */
1006: private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
1007: {
1008: $startLine = $tokens->currentTokenLine();
1009: $startIndex = $tokens->currentTokenIndex();
1010:
1011: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
1012: $tokens->skipNewLineTokensAndConsumeComments();
1013:
1014: $valueType = $this->parse($tokens);
1015: $tokens->skipNewLineTokensAndConsumeComments();
1016:
1017: $keyType = null;
1018: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
1019: $tokens->skipNewLineTokensAndConsumeComments();
1020:
1021: $keyType = $valueType;
1022: $valueType = $this->parse($tokens);
1023: $tokens->skipNewLineTokensAndConsumeComments();
1024: }
1025:
1026: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
1027:
1028: return $this->enrichWithAttributes(
1029: $tokens,
1030: new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType),
1031: $startLine,
1032: $startIndex,
1033: );
1034: }
1035:
1036: /**
1037: * @phpstan-impure
1038: */
1039: private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode
1040: {
1041: $startLine = $tokens->currentTokenLine();
1042: $startIndex = $tokens->currentTokenIndex();
1043:
1044: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
1045: $tokens->skipNewLineTokensAndConsumeComments();
1046:
1047: $valueType = $this->parse($tokens);
1048: $tokens->skipNewLineTokensAndConsumeComments();
1049:
1050: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
1051:
1052: return $this->enrichWithAttributes(
1053: $tokens,
1054: new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null),
1055: $startLine,
1056: $startIndex,
1057: );
1058: }
1059:
1060: /**
1061: * @phpstan-impure
1062: */
1063: private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode
1064: {
1065: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
1066:
1067: $items = [];
1068:
1069: do {
1070: $tokens->skipNewLineTokensAndConsumeComments();
1071:
1072: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
1073: return new Ast\Type\ObjectShapeNode($items);
1074: }
1075:
1076: $items[] = $this->parseObjectShapeItem($tokens);
1077:
1078: $tokens->skipNewLineTokensAndConsumeComments();
1079: } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
1080:
1081: $tokens->skipNewLineTokensAndConsumeComments();
1082: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
1083:
1084: return new Ast\Type\ObjectShapeNode($items);
1085: }
1086:
1087: /** @phpstan-impure */
1088: private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode
1089: {
1090: $startLine = $tokens->currentTokenLine();
1091: $startIndex = $tokens->currentTokenIndex();
1092:
1093: $tokens->skipNewLineTokensAndConsumeComments();
1094:
1095: $key = $this->parseObjectShapeKey($tokens);
1096: $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
1097: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
1098: $value = $this->parse($tokens);
1099:
1100: return $this->enrichWithAttributes(
1101: $tokens,
1102: new Ast\Type\ObjectShapeItemNode($key, $optional, $value),
1103: $startLine,
1104: $startIndex,
1105: );
1106: }
1107:
1108: /**
1109: * @phpstan-impure
1110: * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
1111: */
1112: private function parseObjectShapeKey(TokenIterator $tokens)
1113: {
1114: $startLine = $tokens->currentTokenLine();
1115: $startIndex = $tokens->currentTokenIndex();
1116:
1117: if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
1118: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED);
1119: $tokens->next();
1120:
1121: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
1122: $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED);
1123: $tokens->next();
1124:
1125: } else {
1126: $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
1127: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
1128: }
1129:
1130: return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
1131: }
1132:
1133: }
1134: