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