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