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