Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

TypeParser.php

Namespace

PHPStan\PhpDocParser\Parser

File

vendor/phpstan/phpdoc-parser/src/Parser/TypeParser.php

View source
<?php

declare (strict_types=1);
namespace PHPStan\PhpDocParser\Parser;

use LogicException;
use PHPStan\PhpDocParser\Ast;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use function in_array;
use function str_replace;
use function strlen;
use function strpos;
use function substr_compare;
use function trim;
class TypeParser {
    
    /** @var ConstExprParser|null */
    private $constExprParser;
    
    /** @var bool */
    private $quoteAwareConstExprString;
    
    /** @var bool */
    private $useLinesAttributes;
    
    /** @var bool */
    private $useIndexAttributes;
    
    /**
     * @param array{lines?: bool, indexes?: bool} $usedAttributes
     */
    public function __construct(?ConstExprParser $constExprParser = null, bool $quoteAwareConstExprString = false, array $usedAttributes = []) {
        $this->constExprParser = $constExprParser;
        $this->quoteAwareConstExprString = $quoteAwareConstExprString;
        $this->useLinesAttributes = $usedAttributes['lines'] ?? false;
        $this->useIndexAttributes = $usedAttributes['indexes'] ?? false;
    }
    
    /** @phpstan-impure */
    public function parse(TokenIterator $tokens) : Ast\Type\TypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
            $type = $this->parseNullable($tokens);
        }
        else {
            $type = $this->parseAtomic($tokens);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
                $type = $this->parseUnion($tokens, $type);
            }
            elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
                $type = $this->parseIntersection($tokens, $type);
            }
        }
        return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
    }
    
    /**
     * @internal
     * @template T of Ast\Node
     * @param T $type
     * @return T
     */
    public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex) : Ast\Node {
        if ($this->useLinesAttributes) {
            $type->setAttribute(Ast\Attribute::START_LINE, $startLine);
            $type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine());
        }
        if ($this->useIndexAttributes) {
            $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex);
            $type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken());
        }
        return $type;
    }
    
    /** @phpstan-impure */
    private function subParse(TokenIterator $tokens) : Ast\Type\TypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
            $type = $this->parseNullable($tokens);
        }
        elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
            $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue());
        }
        else {
            $type = $this->parseAtomic($tokens);
            if ($tokens->isCurrentTokenValue('is')) {
                $type = $this->parseConditional($tokens, $type);
            }
            else {
                $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
                if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) {
                    $type = $this->subParseUnion($tokens, $type);
                }
                elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) {
                    $type = $this->subParseIntersection($tokens, $type);
                }
            }
        }
        return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
    }
    
    /** @phpstan-impure */
    private function parseAtomic(TokenIterator $tokens) : Ast\Type\TypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            $type = $this->subParse($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
            }
            return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
        }
        if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
            $type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
            }
            return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
        }
        $currentTokenValue = $tokens->currentTokenValue();
        $tokens->pushSavePoint();
        // because of ConstFetchNode
        if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
            $type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex);
            if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
                $tokens->dropSavePoint();
                // because of ConstFetchNode
                if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
                    $tokens->pushSavePoint();
                    $isHtml = $this->isHtml($tokens);
                    $tokens->rollback();
                    if ($isHtml) {
                        return $type;
                    }
                    $origType = $type;
                    $type = $this->tryParseCallable($tokens, $type, true);
                    if ($type === $origType) {
                        $type = $this->parseGeneric($tokens, $type);
                        if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                            $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
                        }
                    }
                }
                elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
                    $type = $this->tryParseCallable($tokens, $type, false);
                }
                elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                    $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
                }
                elseif (in_array($type->name, [
                    Ast\Type\ArrayShapeNode::KIND_ARRAY,
                    Ast\Type\ArrayShapeNode::KIND_LIST,
                    Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY,
                    Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST,
                    'object',
                ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
                    if ($type->name === 'object') {
                        $type = $this->parseObjectShape($tokens);
                    }
                    else {
                        $type = $this->parseArrayShape($tokens, $type, $type->name);
                    }
                    if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                        $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
                    }
                }
                return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
            }
            else {
                $tokens->rollback();
                // because of ConstFetchNode
            }
        }
        else {
            $tokens->dropSavePoint();
            // because of ConstFetchNode
        }
        $currentTokenValue = $tokens->currentTokenValue();
        $currentTokenType = $tokens->currentTokenType();
        $currentTokenOffset = $tokens->currentTokenOffset();
        $currentTokenLine = $tokens->currentTokenLine();
        if ($this->constExprParser === null) {
            throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
        }
        try {
            $constExpr = $this->constExprParser
                ->parse($tokens, true);
            if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
                throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
            }
            $type = $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
            }
            return $type;
        } catch (LogicException $e) {
            throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
        }
    }
    
    /** @phpstan-impure */
    private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type) : Ast\Type\TypeNode {
        $types = [
            $type,
        ];
        while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
            $types[] = $this->parseAtomic($tokens);
        }
        return new Ast\Type\UnionTypeNode($types);
    }
    
    /** @phpstan-impure */
    private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type) : Ast\Type\TypeNode {
        $types = [
            $type,
        ];
        while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            $types[] = $this->parseAtomic($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        }
        return new Ast\Type\UnionTypeNode($types);
    }
    
    /** @phpstan-impure */
    private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type) : Ast\Type\TypeNode {
        $types = [
            $type,
        ];
        while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
            $types[] = $this->parseAtomic($tokens);
        }
        return new Ast\Type\IntersectionTypeNode($types);
    }
    
    /** @phpstan-impure */
    private function subParseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type) : Ast\Type\TypeNode {
        $types = [
            $type,
        ];
        while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            $types[] = $this->parseAtomic($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        }
        return new Ast\Type\IntersectionTypeNode($types);
    }
    
    /** @phpstan-impure */
    private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType) : Ast\Type\TypeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        $negated = false;
        if ($tokens->isCurrentTokenValue('not')) {
            $negated = true;
            $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        }
        $targetType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $ifType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_COLON);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $elseType = $this->subParse($tokens);
        return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated);
    }
    
    /** @phpstan-impure */
    private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName) : Ast\Type\TypeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
        $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is');
        $negated = false;
        if ($tokens->isCurrentTokenValue('not')) {
            $negated = true;
            $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        }
        $targetType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $ifType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_COLON);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $elseType = $this->subParse($tokens);
        return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated);
    }
    
    /** @phpstan-impure */
    private function parseNullable(TokenIterator $tokens) : Ast\Type\TypeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE);
        $type = $this->parseAtomic($tokens);
        return new Ast\Type\NullableTypeNode($type);
    }
    
    /** @phpstan-impure */
    public function isHtml(TokenIterator $tokens) : bool {
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
        if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) {
            return false;
        }
        $htmlTagName = $tokens->currentTokenValue();
        $tokens->next();
        if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
            return false;
        }
        $endTag = '</' . $htmlTagName . '>';
        $endTagSearchOffset = -strlen($endTag);
        while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) {
            if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0) {
                return true;
            }
            $tokens->next();
        }
        return false;
    }
    
    /** @phpstan-impure */
    public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType) : Ast\Type\GenericTypeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
        $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE);
        $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX);
        $genericTypes = [];
        $variances = [];
        $isFirst = true;
        while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            // trailing comma case
            if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
                break;
            }
            $isFirst = false;
            [
                $genericTypes[],
                $variances[],
            ] = $this->parseGenericTypeArgument($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        }
        $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances);
        if ($startLine !== null && $startIndex !== null) {
            $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
        }
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
        return $type;
    }
    
    /**
     * @phpstan-impure
     * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*}
     */
    public function parseGenericTypeArgument(TokenIterator $tokens) : array {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) {
            return [
                $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex),
                Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT,
            ];
        }
        if ($tokens->tryConsumeTokenValue('contravariant')) {
            $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT;
        }
        elseif ($tokens->tryConsumeTokenValue('covariant')) {
            $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT;
        }
        else {
            $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT;
        }
        $type = $this->parse($tokens);
        return [
            $type,
            $variance,
        ];
    }
    
    /**
     * @throws ParserException
     * @param ?callable(TokenIterator): string $parseDescription
     */
    public function parseTemplateTagValue(TokenIterator $tokens, ?callable $parseDescription = null) : TemplateTagValueNode {
        $name = $tokens->currentTokenValue();
        $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        $upperBound = $lowerBound = null;
        if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) {
            $upperBound = $this->parse($tokens);
        }
        if ($tokens->tryConsumeTokenValue('super')) {
            $lowerBound = $this->parse($tokens);
        }
        if ($tokens->tryConsumeTokenValue('=')) {
            $default = $this->parse($tokens);
        }
        else {
            $default = null;
        }
        if ($parseDescription !== null) {
            $description = $parseDescription($tokens);
        }
        else {
            $description = '';
        }
        if ($name === '') {
            throw new LogicException('Template tag name cannot be empty.');
        }
        return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound);
    }
    
    /** @phpstan-impure */
    private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate) : Ast\Type\TypeNode {
        $templates = $hasTemplate ? $this->parseCallableTemplates($tokens) : [];
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $parameters = [];
        if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
            $parameters[] = $this->parseCallableParameter($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
                $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
                if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) {
                    break;
                }
                $parameters[] = $this->parseCallableParameter($tokens);
                $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            }
        }
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
        $tokens->consumeTokenType(Lexer::TOKEN_COLON);
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex);
        return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates);
    }
    
    /**
     * @return Ast\PhpDoc\TemplateTagValueNode[]
     *
     * @phpstan-impure
     */
    private function parseCallableTemplates(TokenIterator $tokens) : array {
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
        $templates = [];
        $isFirst = true;
        while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            // trailing comma case
            if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) {
                break;
            }
            $isFirst = false;
            $templates[] = $this->parseCallableTemplateArgument($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        }
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
        return $templates;
    }
    private function parseCallableTemplateArgument(TokenIterator $tokens) : Ast\PhpDoc\TemplateTagValueNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        return $this->enrichWithAttributes($tokens, $this->parseTemplateTagValue($tokens), $startLine, $startIndex);
    }
    
    /** @phpstan-impure */
    private function parseCallableParameter(TokenIterator $tokens) : Ast\Type\CallableTypeParameterNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        $type = $this->parse($tokens);
        $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE);
        $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC);
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) {
            $parameterName = $tokens->currentTokenValue();
            $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE);
        }
        else {
            $parameterName = '';
        }
        $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL);
        return $this->enrichWithAttributes($tokens, new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional), $startLine, $startIndex);
    }
    
    /** @phpstan-impure */
    private function parseCallableReturnType(TokenIterator $tokens) : Ast\Type\TypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) {
            return $this->parseNullable($tokens);
        }
        elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) {
            $type = $this->subParse($tokens);
            $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
            }
            return $type;
        }
        elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) {
            $type = new Ast\Type\ThisTypeNode();
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
            }
            return $type;
        }
        else {
            $currentTokenValue = $tokens->currentTokenValue();
            $tokens->pushSavePoint();
            // because of ConstFetchNode
            if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) {
                $type = new Ast\Type\IdentifierTypeNode($currentTokenValue);
                if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) {
                    if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
                        $type = $this->parseGeneric($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
                        if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                            $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
                        }
                    }
                    elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                        $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
                    }
                    elseif (in_array($type->name, [
                        Ast\Type\ArrayShapeNode::KIND_ARRAY,
                        Ast\Type\ArrayShapeNode::KIND_LIST,
                        Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY,
                        Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST,
                        'object',
                    ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) {
                        if ($type->name === 'object') {
                            $type = $this->parseObjectShape($tokens);
                        }
                        else {
                            $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex), $type->name);
                        }
                        if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                            $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex));
                        }
                    }
                    return $type;
                }
                else {
                    $tokens->rollback();
                    // because of ConstFetchNode
                }
            }
            else {
                $tokens->dropSavePoint();
                // because of ConstFetchNode
            }
        }
        $currentTokenValue = $tokens->currentTokenValue();
        $currentTokenType = $tokens->currentTokenType();
        $currentTokenOffset = $tokens->currentTokenOffset();
        $currentTokenLine = $tokens->currentTokenLine();
        if ($this->constExprParser === null) {
            throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
        }
        try {
            $constExpr = $this->constExprParser
                ->parse($tokens, true);
            if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) {
                throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
            }
            $type = $this->enrichWithAttributes($tokens, new Ast\Type\ConstTypeNode($constExpr), $startLine, $startIndex);
            if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $type = $this->tryParseArrayOrOffsetAccess($tokens, $type);
            }
            return $type;
        } catch (LogicException $e) {
            throw new ParserException($currentTokenValue, $currentTokenType, $currentTokenOffset, Lexer::TOKEN_IDENTIFIER, null, $currentTokenLine);
        }
    }
    
    /** @phpstan-impure */
    private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate) : Ast\Type\TypeNode {
        try {
            $tokens->pushSavePoint();
            $type = $this->parseCallable($tokens, $identifier, $hasTemplate);
            $tokens->dropSavePoint();
        } catch (ParserException $e) {
            $tokens->rollback();
            $type = $identifier;
        }
        return $type;
    }
    
    /** @phpstan-impure */
    private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type) : Ast\Type\TypeNode {
        $startLine = $type->getAttribute(Ast\Attribute::START_LINE);
        $startIndex = $type->getAttribute(Ast\Attribute::START_INDEX);
        try {
            while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) {
                $tokens->pushSavePoint();
                $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace();
                $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET);
                if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) {
                    $offset = $this->parse($tokens);
                    $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
                    $tokens->dropSavePoint();
                    $type = new Ast\Type\OffsetAccessTypeNode($type, $offset);
                    if ($startLine !== null && $startIndex !== null) {
                        $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
                    }
                }
                else {
                    $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET);
                    $tokens->dropSavePoint();
                    $type = new Ast\Type\ArrayTypeNode($type);
                    if ($startLine !== null && $startIndex !== null) {
                        $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex);
                    }
                }
            }
        } catch (ParserException $e) {
            $tokens->rollback();
        }
        return $type;
    }
    
    /**
     * @phpstan-impure
     * @param Ast\Type\ArrayShapeNode::KIND_* $kind
     */
    private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind) : Ast\Type\ArrayShapeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
        $items = [];
        $sealed = true;
        $unsealedType = null;
        do {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
                return new Ast\Type\ArrayShapeNode($items, true, $kind);
            }
            if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) {
                $sealed = false;
                $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
                if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) {
                    if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) {
                        $unsealedType = $this->parseArrayShapeUnsealedType($tokens);
                    }
                    else {
                        $unsealedType = $this->parseListShapeUnsealedType($tokens);
                    }
                    $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
                }
                $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA);
                break;
            }
            $items[] = $this->parseArrayShapeItem($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
        return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $unsealedType);
    }
    
    /** @phpstan-impure */
    private function parseArrayShapeItem(TokenIterator $tokens) : Ast\Type\ArrayShapeItemNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        try {
            $tokens->pushSavePoint();
            $key = $this->parseArrayShapeKey($tokens);
            $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
            $tokens->consumeTokenType(Lexer::TOKEN_COLON);
            $value = $this->parse($tokens);
            $tokens->dropSavePoint();
            return $this->enrichWithAttributes($tokens, new Ast\Type\ArrayShapeItemNode($key, $optional, $value), $startLine, $startIndex);
        } catch (ParserException $e) {
            $tokens->rollback();
            $value = $this->parse($tokens);
            return $this->enrichWithAttributes($tokens, new Ast\Type\ArrayShapeItemNode(null, false, $value), $startLine, $startIndex);
        }
    }
    
    /**
     * @phpstan-impure
     * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
     */
    private function parseArrayShapeKey(TokenIterator $tokens) {
        $startIndex = $tokens->currentTokenIndex();
        $startLine = $tokens->currentTokenLine();
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) {
            $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue()));
            $tokens->next();
        }
        elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
            if ($this->quoteAwareConstExprString) {
                $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
            }
            else {
                $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
            }
            $tokens->next();
        }
        elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
            if ($this->quoteAwareConstExprString) {
                $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
            }
            else {
                $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
            }
            $tokens->next();
        }
        else {
            $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
            $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        }
        return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
    }
    
    /**
     * @phpstan-impure
     */
    private function parseArrayShapeUnsealedType(TokenIterator $tokens) : Ast\Type\ArrayShapeUnsealedTypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $valueType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $keyType = null;
        if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            $keyType = $valueType;
            $valueType = $this->parse($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        }
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
        return $this->enrichWithAttributes($tokens, new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType), $startLine, $startIndex);
    }
    
    /**
     * @phpstan-impure
     */
    private function parseListShapeUnsealedType(TokenIterator $tokens) : Ast\Type\ArrayShapeUnsealedTypeNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $valueType = $this->parse($tokens);
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET);
        return $this->enrichWithAttributes($tokens, new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null), $startLine, $startIndex);
    }
    
    /**
     * @phpstan-impure
     */
    private function parseObjectShape(TokenIterator $tokens) : Ast\Type\ObjectShapeNode {
        $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET);
        $items = [];
        do {
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
            if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) {
                return new Ast\Type\ObjectShapeNode($items);
            }
            $items[] = $this->parseObjectShapeItem($tokens);
            $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA));
        $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL);
        $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET);
        return new Ast\Type\ObjectShapeNode($items);
    }
    
    /** @phpstan-impure */
    private function parseObjectShapeItem(TokenIterator $tokens) : Ast\Type\ObjectShapeItemNode {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        $key = $this->parseObjectShapeKey($tokens);
        $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE);
        $tokens->consumeTokenType(Lexer::TOKEN_COLON);
        $value = $this->parse($tokens);
        return $this->enrichWithAttributes($tokens, new Ast\Type\ObjectShapeItemNode($key, $optional, $value), $startLine, $startIndex);
    }
    
    /**
     * @phpstan-impure
     * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode
     */
    private function parseObjectShapeKey(TokenIterator $tokens) {
        $startLine = $tokens->currentTokenLine();
        $startIndex = $tokens->currentTokenIndex();
        if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) {
            if ($this->quoteAwareConstExprString) {
                $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::SINGLE_QUOTED);
            }
            else {
                $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), "'"));
            }
            $tokens->next();
        }
        elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) {
            if ($this->quoteAwareConstExprString) {
                $key = new Ast\ConstExpr\QuoteAwareConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\QuoteAwareConstExprStringNode::DOUBLE_QUOTED);
            }
            else {
                $key = new Ast\ConstExpr\ConstExprStringNode(trim($tokens->currentTokenValue(), '"'));
            }
            $tokens->next();
        }
        else {
            $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue());
            $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER);
        }
        return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex);
    }

}

Classes

Title Deprecated Summary
TypeParser
RSS feed
Powered by Drupal