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