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