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