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\Lexer\Lexer;
8: use function strpos;
9: use function trim;
10:
11: class TypeParser
12: {
13:
14: /** @var ConstExprParser|null */
15: private $constExprParser;
16:
17: public function __construct(?ConstExprParser $constExprParser = null)
18: {
19: $this->constExprParser = $constExprParser;
20: }
21:
22: /** @phpstan-impure */
23: public function parse(TokenIterator $tokens): Ast\Type\TypeNode
24: {
25: if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
26: $type = $this->parseNullable($tokens);
27:
28: } else {
29: $type = $this->parseAtomic($tokens);
30:
31: if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
32: $type = $this->parseUnion($tokens, $type);
33:
34: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
35: $type = $this->parseIntersection($tokens, $type);
36: }
37: }
38:
39: return $type;
40: }
41:
42: /** @phpstan-impure */
43: private function subParse(TokenIterator $tokens): Ast\Type\TypeNode
44: {
45: if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
46: $type = $this->parseNullable($tokens);
47:
48: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
49: $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());
50:
51: } else {
52: $type = $this->parseAtomic($tokens);
53:
54: if ($tokens->isCurrentTokenValue('is')) {
55: $type = $this->parseConditional($tokens, $type);
56: } else {
57: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
58:
59: if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
60: $type = $this->subParseUnion($tokens, $type);
61:
62: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
63: $type = $this->subParseIntersection($tokens, $type);
64: }
65: }
66: }
67:
68: return $type;
69: }
70:
71:
72: /** @phpstan-impure */
73: private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode
74: {
75: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
76: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
77: $type = $this->subParse($tokens);
78: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
79:
80: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
81:
82: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
83: return $this->tryParseArrayOrOffsetAccess($tokens, $type);
84: }
85:
86: return $type;
87: }
88:
89: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
90: $type = new Ast\Type\ThisTypeNode();
91:
92: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
93: return $this->tryParseArrayOrOffsetAccess($tokens, $type);
94: }
95:
96: return $type;
97: }
98:
99: $currentTokenValue = $tokens->currentTokenValue();
100: $tokens->pushSavePoint(); // because of ConstFetchNode
101: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
102: $type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
103:
104: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
105: $tokens->dropSavePoint(); // because of ConstFetchNode
106: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
107: $tokens->pushSavePoint();
108:
109: $isHtml = $this->isHtml($tokens);
110: $tokens->rollback();
111: if ($isHtml) {
112: return $type;
113: }
114:
115: $type = $this->parseGeneric($tokens, $type);
116:
117: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
118: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
119: }
120: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
121: $type = $this->tryParseCallable($tokens, $type);
122:
123: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
124: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
125:
126: } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
127: $type = $this->parseArrayShape($tokens, $type);
128:
129: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
130: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
131: }
132: }
133:
134: return $type;
135: } else {
136: $tokens->rollback(); // because of ConstFetchNode
137: }
138: } else {
139: $tokens->dropSavePoint(); // because of ConstFetchNode
140: }
141:
142: $exception = new ParserException(
143: $tokens->currentTokenValue(),
144: $tokens->currentTokenType(),
145: $tokens->currentTokenOffset(),
146: Lexer::TOKEN_IDENTIFIER
147: );
148:
149: if ($this->constExprParser === null) {
150: throw $exception;
151: }
152:
153: try {
154: $constExpr = $this->constExprParser->parse($tokens, true);
155: if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
156: throw $exception;
157: }
158:
159: return new Ast\Type\ConstTypeNode($constExpr);
160: } catch (LogicException $e) {
161: throw $exception;
162: }
163: }
164:
165:
166: /** @phpstan-impure */
167: private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
168: {
169: $types = [$type];
170:
171: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
172: $types[] = $this->parseAtomic($tokens);
173: }
174:
175: return new Ast\Type\UnionTypeNode($types);
176: }
177:
178:
179: /** @phpstan-impure */
180: private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
181: {
182: $types = [$type];
183:
184: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
185: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
186: $types[] = $this->parseAtomic($tokens);
187: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
188: }
189:
190: return new Ast\Type\UnionTypeNode($types);
191: }
192:
193:
194: /** @phpstan-impure */
195: private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
196: {
197: $types = [$type];
198:
199: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
200: $types[] = $this->parseAtomic($tokens);
201: }
202:
203: return new Ast\Type\IntersectionTypeNode($types);
204: }
205:
206:
207: /** @phpstan-impure */
208: private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
209: {
210: $types = [$type];
211:
212: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
213: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
214: $types[] = $this->parseAtomic($tokens);
215: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
216: }
217:
218: return new Ast\Type\IntersectionTypeNode($types);
219: }
220:
221:
222: /** @phpstan-impure */
223: private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode
224: {
225: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
226:
227: $negated = false;
228: if ($tokens->isCurrentTokenValue('not')) {
229: $negated = true;
230: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
231: }
232:
233: $targetType = $this->parse($tokens);
234:
235: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
236: $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
237: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
238:
239: $ifType = $this->parse($tokens);
240:
241: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
242: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
243: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
244:
245: $elseType = $this->parse($tokens);
246:
247: return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated);
248: }
249:
250: /** @phpstan-impure */
251: private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode
252: {
253: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
254: $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');
255:
256: $negated = false;
257: if ($tokens->isCurrentTokenValue('not')) {
258: $negated = true;
259: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
260: }
261:
262: $targetType = $this->parse($tokens);
263:
264: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
265: $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
266: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
267:
268: $ifType = $this->parse($tokens);
269:
270: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
271: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
272: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
273:
274: $elseType = $this->parse($tokens);
275:
276: return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
277: }
278:
279:
280: /** @phpstan-impure */
281: private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode
282: {
283: $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
284:
285: $type = $this->parseAtomic($tokens);
286:
287: return new Ast\Type\NullableTypeNode($type);
288: }
289:
290: /** @phpstan-impure */
291: public function isHtml(TokenIterator $tokens): bool
292: {
293: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
294:
295: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
296: return false;
297: }
298:
299: $htmlTagName = $tokens->currentTokenValue();
300:
301: $tokens->next();
302:
303: if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
304: return false;
305: }
306:
307: while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
308: if (
309: $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)
310: && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false
311: ) {
312: return true;
313: }
314:
315: $tokens->next();
316: }
317:
318: return false;
319: }
320:
321: /** @phpstan-impure */
322: public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode
323: {
324: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
325: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
326: $genericTypes = [$this->parse($tokens)];
327:
328: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
329:
330: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
331: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
332: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
333: // trailing comma case
334: return new Ast\Type\GenericTypeNode($baseType, $genericTypes);
335: }
336: $genericTypes[] = $this->parse($tokens);
337: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
338: }
339:
340: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
341: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
342:
343: return new Ast\Type\GenericTypeNode($baseType, $genericTypes);
344: }
345:
346:
347: /** @phpstan-impure */
348: private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
349: {
350: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
351: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
352:
353: $parameters = [];
354: if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
355: $parameters[] = $this->parseCallableParameter($tokens);
356: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
357: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
358: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
359: if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
360: break;
361: }
362: $parameters[] = $this->parseCallableParameter($tokens);
363: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
364: }
365: }
366:
367: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
368: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
369: $returnType = $this->parseCallableReturnType($tokens);
370:
371: return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType);
372: }
373:
374:
375: /** @phpstan-impure */
376: private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode
377: {
378: $type = $this->parse($tokens);
379: $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
380: $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
381:
382: if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
383: $parameterName = $tokens->currentTokenValue();
384: $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
385:
386: } else {
387: $parameterName = '';
388: }
389:
390: $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
391: return new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional);
392: }
393:
394:
395: /** @phpstan-impure */
396: private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode
397: {
398: if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
399: $type = $this->parseNullable($tokens);
400:
401: } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
402: $type = $this->parse($tokens);
403: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
404:
405: } else {
406: $type = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
407: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
408:
409: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
410: $type = $this->parseGeneric($tokens, $type);
411:
412: } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
413: $type = $this->parseArrayShape($tokens, $type);
414: }
415: }
416:
417: if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
418: $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
419: }
420:
421: return $type;
422: }
423:
424:
425: /** @phpstan-impure */
426: private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier): Ast\Type\TypeNode
427: {
428: try {
429: $tokens->pushSavePoint();
430: $type = $this->parseCallable($tokens, $identifier);
431: $tokens->dropSavePoint();
432:
433: } catch (ParserException $e) {
434: $tokens->rollback();
435: $type = $identifier;
436: }
437:
438: return $type;
439: }
440:
441:
442: /** @phpstan-impure */
443: private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode
444: {
445: try {
446: while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
447: $tokens->pushSavePoint();
448:
449: $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace();
450: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET);
451:
452: if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) {
453: $offset = $this->parse($tokens);
454: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
455: $tokens->dropSavePoint();
456: $type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
457: } else {
458: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
459: $tokens->dropSavePoint();
460: $type = new Ast\Type\ArrayTypeNode($type);
461: }
462: }
463:
464: } catch (ParserException $e) {
465: $tokens->rollback();
466: }
467:
468: return $type;
469: }
470:
471:
472: /** @phpstan-impure */
473: private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode
474: {
475: $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
476: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
477: return new Ast\Type\ArrayShapeNode([]);
478: }
479:
480: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
481: $items = [$this->parseArrayShapeItem($tokens)];
482:
483: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
484: while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
485: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
486: if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
487: // trailing comma case
488: return new Ast\Type\ArrayShapeNode($items);
489: }
490:
491: $items[] = $this->parseArrayShapeItem($tokens);
492: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
493: }
494:
495: $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
496: $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
497:
498: return new Ast\Type\ArrayShapeNode($items);
499: }
500:
501:
502: /** @phpstan-impure */
503: private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode
504: {
505: try {
506: $tokens->pushSavePoint();
507: $key = $this->parseArrayShapeKey($tokens);
508: $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
509: $tokens->consumeTokenType(Lexer::TOKEN_COLON);
510: $value = $this->parse($tokens);
511: $tokens->dropSavePoint();
512:
513: return new Ast\Type\ArrayShapeItemNode($key, $optional, $value);
514: } catch (ParserException $e) {
515: $tokens->rollback();
516: $value = $this->parse($tokens);
517:
518: return new Ast\Type\ArrayShapeItemNode(null, false, $value);
519: }
520: }
521:
522: /**
523: * @phpstan-impure
524: * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
525: */
526: private function parseArrayShapeKey(TokenIterator $tokens)
527: {
528: if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
529: $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue());
530: $tokens->next();
531:
532: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
533: $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
534: $tokens->next();
535:
536: } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
537: $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
538: $tokens->next();
539:
540: } else {
541: $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
542: $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
543: }
544:
545: return $key;
546: }
547:
548: }
549: