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