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