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