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