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