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