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