├── src ├── Ast │ ├── Type │ │ ├── TypeNode.php │ │ ├── ThisTypeNode.php │ │ ├── IdentifierTypeNode.php │ │ ├── NullableTypeNode.php │ │ ├── ConstTypeNode.php │ │ ├── ObjectShapeNode.php │ │ ├── ArrayTypeNode.php │ │ ├── OffsetAccessTypeNode.php │ │ ├── ArrayShapeUnsealedTypeNode.php │ │ ├── UnionTypeNode.php │ │ ├── IntersectionTypeNode.php │ │ ├── InvalidTypeNode.php │ │ ├── ConditionalTypeNode.php │ │ ├── ConditionalTypeForParameterNode.php │ │ ├── ObjectShapeItemNode.php │ │ ├── CallableTypeParameterNode.php │ │ ├── ArrayShapeItemNode.php │ │ ├── CallableTypeNode.php │ │ ├── GenericTypeNode.php │ │ └── ArrayShapeNode.php │ ├── PhpDoc │ │ ├── PhpDocChildNode.php │ │ ├── PhpDocTagValueNode.php │ │ ├── PhpDocTextNode.php │ │ ├── GenericTagValueNode.php │ │ ├── DeprecatedTagValueNode.php │ │ ├── TypeAliasTagValueNode.php │ │ ├── Doctrine │ │ │ ├── DoctrineArray.php │ │ │ ├── DoctrineAnnotation.php │ │ │ ├── DoctrineTagValueNode.php │ │ │ ├── DoctrineArgument.php │ │ │ └── DoctrineArrayItem.php │ │ ├── MixinTagValueNode.php │ │ ├── ReturnTagValueNode.php │ │ ├── SealedTagValueNode.php │ │ ├── ThrowsTagValueNode.php │ │ ├── SelfOutTagValueNode.php │ │ ├── RequireExtendsTagValueNode.php │ │ ├── UsesTagValueNode.php │ │ ├── ExtendsTagValueNode.php │ │ ├── RequireImplementsTagValueNode.php │ │ ├── ImplementsTagValueNode.php │ │ ├── ParamLaterInvokedCallableTagValueNode.php │ │ ├── PureUnlessCallableIsImpureTagValueNode.php │ │ ├── ParamImmediatelyInvokedCallableTagValueNode.php │ │ ├── PhpDocTagNode.php │ │ ├── PropertyTagValueNode.php │ │ ├── ParamOutTagValueNode.php │ │ ├── ParamClosureThisTagValueNode.php │ │ ├── VarTagValueNode.php │ │ ├── TypeAliasImportTagValueNode.php │ │ ├── TypelessParamTagValueNode.php │ │ ├── AssertTagValueNode.php │ │ ├── ParamTagValueNode.php │ │ ├── AssertTagMethodValueNode.php │ │ ├── AssertTagPropertyValueNode.php │ │ ├── MethodTagValueParameterNode.php │ │ ├── TemplateTagValueNode.php │ │ ├── InvalidTagValueNode.php │ │ ├── MethodTagValueNode.php │ │ └── PhpDocNode.php │ ├── ConstExpr │ │ ├── ConstExprNode.php │ │ ├── ConstExprFalseNode.php │ │ ├── ConstExprNullNode.php │ │ ├── ConstExprTrueNode.php │ │ ├── ConstExprFloatNode.php │ │ ├── ConstExprIntegerNode.php │ │ ├── ConstExprArrayNode.php │ │ ├── ConstExprArrayItemNode.php │ │ ├── ConstFetchNode.php │ │ ├── DoctrineConstExprStringNode.php │ │ └── ConstExprStringNode.php │ ├── Attribute.php │ ├── Node.php │ ├── NodeVisitor │ │ └── CloningVisitor.php │ ├── Comment.php │ ├── AbstractNodeVisitor.php │ ├── NodeAttributes.php │ ├── NodeVisitor.php │ └── NodeTraverser.php ├── ParserConfig.php ├── Printer │ ├── DiffElem.php │ ├── Differ.php │ └── Printer.php ├── Parser │ ├── ParserException.php │ ├── StringUnescaper.php │ ├── ConstExprParser.php │ ├── TokenIterator.php │ └── TypeParser.php └── Lexer │ └── Lexer.php ├── composer.json ├── LICENSE ├── README.md └── UPGRADING.md /src/Ast/Type/TypeNode.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->name; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/PhpDocTextNode.php: -------------------------------------------------------------------------------- 1 | text = $text; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->text; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/Type/NullableTypeNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return '?' . $this->type; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstExprFloatNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->value; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstExprIntegerNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | return $this->value; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/GenericTagValueNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 18 | } 19 | 20 | public function __toString(): string 21 | { 22 | return $this->value; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Ast/NodeVisitor/CloningVisitor.php: -------------------------------------------------------------------------------- 1 | setAttribute(Attribute::ORIGINAL_NODE, $originalNode); 16 | 17 | return $node; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/Ast/Comment.php: -------------------------------------------------------------------------------- 1 | text = $text; 19 | $this->startLine = $startLine; 20 | $this->startIndex = $startIndex; 21 | } 22 | 23 | public function getReformattedText(): string 24 | { 25 | return trim($this->text); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/Ast/Type/ConstTypeNode.php: -------------------------------------------------------------------------------- 1 | constExpr = $constExpr; 18 | } 19 | 20 | public function __toString(): string 21 | { 22 | return $this->constExpr->__toString(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/DeprecatedTagValueNode.php: -------------------------------------------------------------------------------- 1 | description = $description; 19 | } 20 | 21 | public function __toString(): string 22 | { 23 | return trim($this->description); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/ParserConfig.php: -------------------------------------------------------------------------------- 1 | useLinesAttributes = $usedAttributes['lines'] ?? false; 20 | $this->useIndexAttributes = $usedAttributes['indexes'] ?? false; 21 | $this->useCommentsAttributes = $usedAttributes['comments'] ?? false; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstExprArrayNode.php: -------------------------------------------------------------------------------- 1 | items = $items; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return '[' . implode(', ', $this->items) . ']'; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Ast/Type/ObjectShapeNode.php: -------------------------------------------------------------------------------- 1 | items = $items; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | $items = $this->items; 27 | 28 | return 'object{' . implode(', ', $items) . '}'; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/TypeAliasTagValueNode.php: -------------------------------------------------------------------------------- 1 | alias = $alias; 21 | $this->type = $type; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return trim("{$this->alias} {$this->type}"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Ast/Type/ArrayTypeNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 17 | } 18 | 19 | public function __toString(): string 20 | { 21 | if ( 22 | $this->type instanceof CallableTypeNode 23 | || $this->type instanceof ConstTypeNode 24 | || $this->type instanceof NullableTypeNode 25 | ) { 26 | return '(' . $this->type . ')[]'; 27 | } 28 | 29 | return $this->type . '[]'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/Doctrine/DoctrineArray.php: -------------------------------------------------------------------------------- 1 | */ 15 | public array $items; 16 | 17 | /** 18 | * @param list $items 19 | */ 20 | public function __construct(array $items) 21 | { 22 | $this->items = $items; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | $items = implode(', ', $this->items); 28 | 29 | return '{' . $items . '}'; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/MixinTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ReturnTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/SealedTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ThrowsTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/SelfOutTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim($this->type . ' ' . $this->description); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/RequireExtendsTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/UsesTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstExprArrayItemNode.php: -------------------------------------------------------------------------------- 1 | key = $key; 20 | $this->value = $value; 21 | } 22 | 23 | public function __toString(): string 24 | { 25 | if ($this->key !== null) { 26 | return sprintf('%s => %s', $this->key, $this->value); 27 | 28 | } 29 | 30 | return (string) $this->value; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ExtendsTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/RequireImplementsTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ImplementsTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 22 | $this->description = $description; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return trim("{$this->type} {$this->description}"); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ParamLaterInvokedCallableTagValueNode.php: -------------------------------------------------------------------------------- 1 | parameterName = $parameterName; 21 | $this->description = $description; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return trim("{$this->parameterName} {$this->description}"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/PureUnlessCallableIsImpureTagValueNode.php: -------------------------------------------------------------------------------- 1 | parameterName = $parameterName; 21 | $this->description = $description; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return trim("{$this->parameterName} {$this->description}"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstFetchNode.php: -------------------------------------------------------------------------------- 1 | className = $className; 20 | $this->name = $name; 21 | } 22 | 23 | public function __toString(): string 24 | { 25 | if ($this->className === '') { 26 | return $this->name; 27 | 28 | } 29 | 30 | return "{$this->className}::{$this->name}"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ParamImmediatelyInvokedCallableTagValueNode.php: -------------------------------------------------------------------------------- 1 | parameterName = $parameterName; 21 | $this->description = $description; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return trim("{$this->parameterName} {$this->description}"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Ast/Type/OffsetAccessTypeNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 19 | $this->offset = $offset; 20 | } 21 | 22 | public function __toString(): string 23 | { 24 | if ( 25 | $this->type instanceof CallableTypeNode 26 | || $this->type instanceof NullableTypeNode 27 | ) { 28 | return '(' . $this->type . ')[' . $this->offset . ']'; 29 | } 30 | 31 | return $this->type . '[' . $this->offset . ']'; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/PhpDocTagNode.php: -------------------------------------------------------------------------------- 1 | name = $name; 21 | $this->value = $value; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | if ($this->value instanceof DoctrineTagValueNode) { 27 | return (string) $this->value; 28 | } 29 | 30 | return trim("{$this->name} {$this->value}"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/Type/ArrayShapeUnsealedTypeNode.php: -------------------------------------------------------------------------------- 1 | valueType = $valueType; 21 | $this->keyType = $keyType; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | if ($this->keyType !== null) { 27 | return sprintf('<%s, %s>', $this->keyType, $this->valueType); 28 | } 29 | return sprintf('<%s>', $this->valueType); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Ast/AbstractNodeVisitor.php: -------------------------------------------------------------------------------- 1 | types = $types; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return '(' . implode(' | ', array_map(static function (TypeNode $type): string { 28 | if ($type instanceof NullableTypeNode) { 29 | return '(' . $type . ')'; 30 | } 31 | 32 | return (string) $type; 33 | }, $this->types)) . ')'; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/Doctrine/DoctrineAnnotation.php: -------------------------------------------------------------------------------- 1 | */ 17 | public array $arguments; 18 | 19 | /** 20 | * @param list $arguments 21 | */ 22 | public function __construct(string $name, array $arguments) 23 | { 24 | $this->name = $name; 25 | $this->arguments = $arguments; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | $arguments = implode(', ', $this->arguments); 31 | return $this->name . '(' . $arguments . ')'; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Ast/Type/IntersectionTypeNode.php: -------------------------------------------------------------------------------- 1 | types = $types; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return '(' . implode(' & ', array_map(static function (TypeNode $type): string { 28 | if ($type instanceof NullableTypeNode) { 29 | return '(' . $type . ')'; 30 | } 31 | 32 | return (string) $type; 33 | }, $this->types)) . ')'; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/Doctrine/DoctrineTagValueNode.php: -------------------------------------------------------------------------------- 1 | annotation = $annotation; 25 | $this->description = $description; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return trim("{$this->annotation} {$this->description}"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/PropertyTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 24 | $this->propertyName = $propertyName; 25 | $this->description = $description; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return trim("{$this->type} {$this->propertyName} {$this->description}"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ParamOutTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 24 | $this->parameterName = $parameterName; 25 | $this->description = $description; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return trim("{$this->type} {$this->parameterName} {$this->description}"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/NodeAttributes.php: -------------------------------------------------------------------------------- 1 | */ 11 | private array $attributes = []; 12 | 13 | /** 14 | * @param mixed $value 15 | */ 16 | public function setAttribute(string $key, $value): void 17 | { 18 | if ($value === null) { 19 | unset($this->attributes[$key]); 20 | return; 21 | } 22 | $this->attributes[$key] = $value; 23 | } 24 | 25 | public function hasAttribute(string $key): bool 26 | { 27 | return array_key_exists($key, $this->attributes); 28 | } 29 | 30 | /** 31 | * @return mixed 32 | */ 33 | public function getAttribute(string $key) 34 | { 35 | if ($this->hasAttribute($key)) { 36 | return $this->attributes[$key]; 37 | } 38 | 39 | return null; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ParamClosureThisTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 24 | $this->parameterName = $parameterName; 25 | $this->description = $description; 26 | } 27 | 28 | public function __toString(): string 29 | { 30 | return trim("{$this->type} {$this->parameterName} {$this->description}"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/VarTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 25 | $this->variableName = $variableName; 26 | $this->description = $description; 27 | } 28 | 29 | public function __toString(): string 30 | { 31 | return trim("$this->type " . trim("{$this->variableName} {$this->description}")); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Ast/Type/InvalidTypeNode.php: -------------------------------------------------------------------------------- 1 | exceptionArgs = [ 19 | $exception->getCurrentTokenValue(), 20 | $exception->getCurrentTokenType(), 21 | $exception->getCurrentOffset(), 22 | $exception->getExpectedTokenType(), 23 | $exception->getExpectedTokenValue(), 24 | $exception->getCurrentTokenLine(), 25 | ]; 26 | } 27 | 28 | public function getException(): ParserException 29 | { 30 | return new ParserException(...$this->exceptionArgs); 31 | } 32 | 33 | public function __toString(): string 34 | { 35 | return '*Invalid type*'; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/TypeAliasImportTagValueNode.php: -------------------------------------------------------------------------------- 1 | importedAlias = $importedAlias; 23 | $this->importedFrom = $importedFrom; 24 | $this->importedAs = $importedAs; 25 | } 26 | 27 | public function __toString(): string 28 | { 29 | return trim( 30 | "{$this->importedAlias} from {$this->importedFrom}" 31 | . ($this->importedAs !== null ? " as {$this->importedAs}" : ''), 32 | ); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/Doctrine/DoctrineArgument.php: -------------------------------------------------------------------------------- 1 | key = $key; 29 | $this->value = $value; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | if ($this->key === null) { 35 | return (string) $this->value; 36 | } 37 | 38 | return $this->key . '=' . $this->value; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Ast/Type/ConditionalTypeNode.php: -------------------------------------------------------------------------------- 1 | subjectType = $subjectType; 26 | $this->targetType = $targetType; 27 | $this->if = $if; 28 | $this->else = $else; 29 | $this->negated = $negated; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | return sprintf( 35 | '(%s %s %s ? %s : %s)', 36 | $this->subjectType, 37 | $this->negated ? 'is not' : 'is', 38 | $this->targetType, 39 | $this->if, 40 | $this->else, 41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/TypelessParamTagValueNode.php: -------------------------------------------------------------------------------- 1 | isReference = $isReference; 25 | $this->isVariadic = $isVariadic; 26 | $this->parameterName = $parameterName; 27 | $this->description = $description; 28 | } 29 | 30 | public function __toString(): string 31 | { 32 | $reference = $this->isReference ? '&' : ''; 33 | $variadic = $this->isVariadic ? '...' : ''; 34 | return trim("{$reference}{$variadic}{$this->parameterName} {$this->description}"); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/Ast/Type/ConditionalTypeForParameterNode.php: -------------------------------------------------------------------------------- 1 | parameterName = $parameterName; 26 | $this->targetType = $targetType; 27 | $this->if = $if; 28 | $this->else = $else; 29 | $this->negated = $negated; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | return sprintf( 35 | '(%s %s %s ? %s : %s)', 36 | $this->parameterName, 37 | $this->negated ? 'is not' : 'is', 38 | $this->targetType, 39 | $this->if, 40 | $this->else, 41 | ); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Printer/DiffElem.php: -------------------------------------------------------------------------------- 1 | type = $type; 40 | $this->old = $old; 41 | $this->new = $new; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpstan/phpdoc-parser", 3 | "description": "PHPDoc parser with support for nullable, intersection and generic types", 4 | "license": "MIT", 5 | "require": { 6 | "php": "^7.4 || ^8.0" 7 | }, 8 | "require-dev": { 9 | "doctrine/annotations": "^2.0", 10 | "nikic/php-parser": "^5.3.0", 11 | "php-parallel-lint/php-parallel-lint": "^1.2", 12 | "phpstan/extension-installer": "^1.0", 13 | "phpstan/phpstan": "^2.0", 14 | "phpstan/phpstan-phpunit": "^2.0", 15 | "phpstan/phpstan-strict-rules": "^2.0", 16 | "phpunit/phpunit": "^9.6", 17 | "symfony/process": "^5.2" 18 | }, 19 | "config": { 20 | "platform": { 21 | "php": "7.4.6" 22 | }, 23 | "sort-packages": true, 24 | "allow-plugins": { 25 | "phpstan/extension-installer": true 26 | } 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "PHPStan\\PhpDocParser\\": [ 31 | "src/" 32 | ] 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "PHPStan\\PhpDocParser\\": [ 38 | "tests/PHPStan" 39 | ] 40 | } 41 | }, 42 | "minimum-stability": "dev", 43 | "prefer-stable": true 44 | } 45 | -------------------------------------------------------------------------------- /src/Ast/Type/ObjectShapeItemNode.php: -------------------------------------------------------------------------------- 1 | keyName = $keyName; 28 | $this->optional = $optional; 29 | $this->valueType = $valueType; 30 | } 31 | 32 | public function __toString(): string 33 | { 34 | if ($this->keyName !== null) { 35 | return sprintf( 36 | '%s%s: %s', 37 | (string) $this->keyName, 38 | $this->optional ? '?' : '', 39 | (string) $this->valueType, 40 | ); 41 | } 42 | 43 | return (string) $this->valueType; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/AssertTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | $this->parameter = $parameter; 29 | $this->isNegated = $isNegated; 30 | $this->isEquality = $isEquality; 31 | $this->description = $description; 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | $isNegated = $this->isNegated ? '!' : ''; 37 | $isEquality = $this->isEquality ? '=' : ''; 38 | return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter} {$this->description}"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Ondřej Mirtes 4 | Copyright (c) 2025 PHPStan s.r.o. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/ParamTagValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | $this->isReference = $isReference; 29 | $this->isVariadic = $isVariadic; 30 | $this->parameterName = $parameterName; 31 | $this->description = $description; 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | $reference = $this->isReference ? '&' : ''; 37 | $variadic = $this->isVariadic ? '...' : ''; 38 | return trim("{$this->type} {$reference}{$variadic}{$this->parameterName} {$this->description}"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/DoctrineConstExprStringNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 22 | } 23 | 24 | public function __toString(): string 25 | { 26 | return self::escape($this->value); 27 | } 28 | 29 | public static function unescape(string $value): string 30 | { 31 | // from https://github.com/doctrine/annotations/blob/a9ec7af212302a75d1f92fa65d3abfbd16245a2a/lib/Doctrine/Common/Annotations/DocLexer.php#L103-L107 32 | return str_replace('""', '"', substr($value, 1, strlen($value) - 2)); 33 | } 34 | 35 | private static function escape(string $value): string 36 | { 37 | // from https://github.com/phpstan/phpdoc-parser/issues/205#issuecomment-1662323656 38 | return sprintf('"%s"', str_replace('"', '""', $value)); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/Ast/Type/CallableTypeParameterNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | $this->isReference = $isReference; 29 | $this->isVariadic = $isVariadic; 30 | $this->parameterName = $parameterName; 31 | $this->isOptional = $isOptional; 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | $type = "{$this->type} "; 37 | $isReference = $this->isReference ? '&' : ''; 38 | $isVariadic = $this->isVariadic ? '...' : ''; 39 | $isOptional = $this->isOptional ? '=' : ''; 40 | return trim("{$type}{$isReference}{$isVariadic}{$this->parameterName}") . $isOptional; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/Doctrine/DoctrineArrayItem.php: -------------------------------------------------------------------------------- 1 | key = $key; 34 | $this->value = $value; 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | if ($this->key === null) { 40 | return (string) $this->value; 41 | } 42 | 43 | return $this->key . '=' . $this->value; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/AssertTagMethodValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 30 | $this->parameter = $parameter; 31 | $this->method = $method; 32 | $this->isNegated = $isNegated; 33 | $this->isEquality = $isEquality; 34 | $this->description = $description; 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | $isNegated = $this->isNegated ? '!' : ''; 40 | $isEquality = $this->isEquality ? '=' : ''; 41 | return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->method}() {$this->description}"); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/AssertTagPropertyValueNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 30 | $this->parameter = $parameter; 31 | $this->property = $property; 32 | $this->isNegated = $isNegated; 33 | $this->isEquality = $isEquality; 34 | $this->description = $description; 35 | } 36 | 37 | public function __toString(): string 38 | { 39 | $isNegated = $this->isNegated ? '!' : ''; 40 | $isEquality = $this->isEquality ? '=' : ''; 41 | return trim("{$isNegated}{$isEquality}{$this->type} {$this->parameter}->{$this->property} {$this->description}"); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/Ast/Type/ArrayShapeItemNode.php: -------------------------------------------------------------------------------- 1 | keyName = $keyName; 30 | $this->optional = $optional; 31 | $this->valueType = $valueType; 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | if ($this->keyName !== null) { 37 | return sprintf( 38 | '%s%s: %s', 39 | (string) $this->keyName, 40 | $this->optional ? '?' : '', 41 | (string) $this->valueType, 42 | ); 43 | } 44 | 45 | return (string) $this->valueType; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/MethodTagValueParameterNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 28 | $this->isReference = $isReference; 29 | $this->isVariadic = $isVariadic; 30 | $this->parameterName = $parameterName; 31 | $this->defaultValue = $defaultValue; 32 | } 33 | 34 | public function __toString(): string 35 | { 36 | $type = $this->type !== null ? "{$this->type} " : ''; 37 | $isReference = $this->isReference ? '&' : ''; 38 | $isVariadic = $this->isVariadic ? '...' : ''; 39 | $default = $this->defaultValue !== null ? " = {$this->defaultValue}" : ''; 40 | return "{$type}{$isReference}{$isVariadic}{$this->parameterName}{$default}"; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/TemplateTagValueNode.php: -------------------------------------------------------------------------------- 1 | name = $name; 32 | $this->bound = $bound; 33 | $this->lowerBound = $lowerBound; 34 | $this->default = $default; 35 | $this->description = $description; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | $upperBound = $this->bound !== null ? " of {$this->bound}" : ''; 41 | $lowerBound = $this->lowerBound !== null ? " super {$this->lowerBound}" : ''; 42 | $default = $this->default !== null ? " = {$this->default}" : ''; 43 | return trim("{$this->name}{$upperBound}{$lowerBound}{$default} {$this->description}"); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/InvalidTagValueNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 28 | $this->exceptionArgs = [ 29 | $exception->getCurrentTokenValue(), 30 | $exception->getCurrentTokenType(), 31 | $exception->getCurrentOffset(), 32 | $exception->getExpectedTokenType(), 33 | $exception->getExpectedTokenValue(), 34 | $exception->getCurrentTokenLine(), 35 | ]; 36 | } 37 | 38 | public function __get(string $name): ?ParserException 39 | { 40 | if ($name !== 'exception') { 41 | trigger_error(sprintf('Undefined property: %s::$%s', self::class, $name), E_USER_WARNING); 42 | return null; 43 | } 44 | 45 | return new ParserException(...$this->exceptionArgs); 46 | } 47 | 48 | public function __toString(): string 49 | { 50 | return $this->value; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/Ast/Type/CallableTypeNode.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 31 | $this->parameters = $parameters; 32 | $this->returnType = $returnType; 33 | $this->templateTypes = $templateTypes; 34 | } 35 | 36 | public function __toString(): string 37 | { 38 | $returnType = $this->returnType; 39 | if ($returnType instanceof self) { 40 | $returnType = "({$returnType})"; 41 | } 42 | $template = $this->templateTypes !== [] 43 | ? '<' . implode(', ', $this->templateTypes) . '>' 44 | : ''; 45 | $parameters = implode(', ', $this->parameters); 46 | return "{$this->identifier}{$template}({$parameters}): {$returnType}"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Ast/Type/GenericTypeNode.php: -------------------------------------------------------------------------------- 1 | type = $type; 34 | $this->genericTypes = $genericTypes; 35 | $this->variances = $variances; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | $genericTypes = []; 41 | 42 | foreach ($this->genericTypes as $index => $type) { 43 | $variance = $this->variances[$index] ?? self::VARIANCE_INVARIANT; 44 | if ($variance === self::VARIANCE_INVARIANT) { 45 | $genericTypes[] = (string) $type; 46 | } elseif ($variance === self::VARIANCE_BIVARIANT) { 47 | $genericTypes[] = '*'; 48 | } else { 49 | $genericTypes[] = sprintf('%s %s', $variance, $type); 50 | } 51 | } 52 | 53 | return $this->type . '<' . implode(', ', $genericTypes) . '>'; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/MethodTagValueNode.php: -------------------------------------------------------------------------------- 1 | isStatic = $isStatic; 37 | $this->returnType = $returnType; 38 | $this->methodName = $methodName; 39 | $this->parameters = $parameters; 40 | $this->description = $description; 41 | $this->templateTypes = $templateTypes; 42 | } 43 | 44 | public function __toString(): string 45 | { 46 | $static = $this->isStatic ? 'static ' : ''; 47 | $returnType = $this->returnType !== null ? "{$this->returnType} " : ''; 48 | $parameters = implode(', ', $this->parameters); 49 | $description = $this->description !== '' ? " {$this->description}" : ''; 50 | $templateTypes = count($this->templateTypes) > 0 ? '<' . implode(', ', $this->templateTypes) . '>' : ''; 51 | return "{$static}{$returnType}{$this->methodName}{$templateTypes}({$parameters}){$description}"; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/Ast/Type/ArrayShapeNode.php: -------------------------------------------------------------------------------- 1 | items = $items; 40 | $this->sealed = $sealed; 41 | $this->unsealedType = $unsealedType; 42 | $this->kind = $kind; 43 | } 44 | 45 | /** 46 | * @param ArrayShapeItemNode[] $items 47 | * @param self::KIND_* $kind 48 | */ 49 | public static function createSealed(array $items, string $kind = self::KIND_ARRAY): self 50 | { 51 | return new self($items, true, null, $kind); 52 | } 53 | 54 | /** 55 | * @param ArrayShapeItemNode[] $items 56 | * @param self::KIND_* $kind 57 | */ 58 | public static function createUnsealed(array $items, ?ArrayShapeUnsealedTypeNode $unsealedType, string $kind = self::KIND_ARRAY): self 59 | { 60 | return new self($items, false, $unsealedType, $kind); 61 | } 62 | 63 | public function __toString(): string 64 | { 65 | $items = $this->items; 66 | 67 | if (! $this->sealed) { 68 | $items[] = '...' . $this->unsealedType; 69 | } 70 | 71 | return $this->kind . '{' . implode(', ', $items) . '}'; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Parser/ParserException.php: -------------------------------------------------------------------------------- 1 | currentTokenValue = $currentTokenValue; 39 | $this->currentTokenType = $currentTokenType; 40 | $this->currentOffset = $currentOffset; 41 | $this->expectedTokenType = $expectedTokenType; 42 | $this->expectedTokenValue = $expectedTokenValue; 43 | $this->currentTokenLine = $currentTokenLine; 44 | 45 | parent::__construct(sprintf( 46 | 'Unexpected token %s, expected %s%s at offset %d%s', 47 | $this->formatValue($currentTokenValue), 48 | Lexer::TOKEN_LABELS[$expectedTokenType], 49 | $expectedTokenValue !== null ? sprintf(' (%s)', $this->formatValue($expectedTokenValue)) : '', 50 | $currentOffset, 51 | $currentTokenLine === null ? '' : sprintf(' on line %d', $currentTokenLine), 52 | )); 53 | } 54 | 55 | public function getCurrentTokenValue(): string 56 | { 57 | return $this->currentTokenValue; 58 | } 59 | 60 | public function getCurrentTokenType(): int 61 | { 62 | return $this->currentTokenType; 63 | } 64 | 65 | public function getCurrentOffset(): int 66 | { 67 | return $this->currentOffset; 68 | } 69 | 70 | public function getExpectedTokenType(): int 71 | { 72 | return $this->expectedTokenType; 73 | } 74 | 75 | public function getExpectedTokenValue(): ?string 76 | { 77 | return $this->expectedTokenValue; 78 | } 79 | 80 | public function getCurrentTokenLine(): ?int 81 | { 82 | return $this->currentTokenLine; 83 | } 84 | 85 | private function formatValue(string $value): string 86 | { 87 | $json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); 88 | assert($json !== false); 89 | 90 | return $json; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/Ast/NodeVisitor.php: -------------------------------------------------------------------------------- 1 | $node stays as-is 33 | * * array (of Nodes) 34 | * => The return value is merged into the parent array (at the position of the $node) 35 | * * NodeTraverser::REMOVE_NODE 36 | * => $node is removed from the parent array 37 | * * NodeTraverser::DONT_TRAVERSE_CHILDREN 38 | * => Children of $node are not traversed. $node stays as-is 39 | * * NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN 40 | * => Further visitors for the current node are skipped, and its children are not 41 | * traversed. $node stays as-is. 42 | * * NodeTraverser::STOP_TRAVERSAL 43 | * => Traversal is aborted. $node stays as-is 44 | * * otherwise 45 | * => $node is set to the return value 46 | * 47 | * @param Node $node Node 48 | * 49 | * @return Node|Node[]|NodeTraverser::*|null Replacement node (or special return value) 50 | */ 51 | public function enterNode(Node $node); 52 | 53 | /** 54 | * Called when leaving a node. 55 | * 56 | * Return value semantics: 57 | * * null 58 | * => $node stays as-is 59 | * * NodeTraverser::REMOVE_NODE 60 | * => $node is removed from the parent array 61 | * * NodeTraverser::STOP_TRAVERSAL 62 | * => Traversal is aborted. $node stays as-is 63 | * * array (of Nodes) 64 | * => The return value is merged into the parent array (at the position of the $node) 65 | * * otherwise 66 | * => $node is set to the return value 67 | * 68 | * @param Node $node Node 69 | * 70 | * @return Node|Node[]|NodeTraverser::REMOVE_NODE|NodeTraverser::STOP_TRAVERSAL|null Replacement node (or special return value) 71 | */ 72 | public function leaveNode(Node $node); 73 | 74 | /** 75 | * Called once after traversal. 76 | * 77 | * Return value semantics: 78 | * * null: $nodes stays as-is 79 | * * otherwise: $nodes is set to the return value 80 | * 81 | * @param Node[] $nodes Array of nodes 82 | * 83 | * @return Node[]|null Array of nodes 84 | */ 85 | public function afterTraverse(array $nodes): ?array; 86 | 87 | } 88 | -------------------------------------------------------------------------------- /src/Parser/StringUnescaper.php: -------------------------------------------------------------------------------- 1 | '\\', 18 | 'n' => "\n", 19 | 'r' => "\r", 20 | 't' => "\t", 21 | 'f' => "\f", 22 | 'v' => "\v", 23 | 'e' => "\x1B", 24 | ]; 25 | 26 | public static function unescapeString(string $string): string 27 | { 28 | $quote = $string[0]; 29 | 30 | if ($quote === '\'') { 31 | return str_replace( 32 | ['\\\\', '\\\''], 33 | ['\\', '\''], 34 | substr($string, 1, -1), 35 | ); 36 | } 37 | 38 | return self::parseEscapeSequences(substr($string, 1, -1), '"'); 39 | } 40 | 41 | /** 42 | * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L90-L130 43 | */ 44 | private static function parseEscapeSequences(string $str, string $quote): string 45 | { 46 | $str = str_replace('\\' . $quote, $quote, $str); 47 | 48 | return preg_replace_callback( 49 | '~\\\\([\\\\nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3}|u\{([0-9a-fA-F]+)\})~', 50 | static function ($matches) { 51 | $str = $matches[1]; 52 | 53 | if (isset(self::REPLACEMENTS[$str])) { 54 | return self::REPLACEMENTS[$str]; 55 | } 56 | if ($str[0] === 'x' || $str[0] === 'X') { 57 | return chr((int) hexdec(substr($str, 1))); 58 | } 59 | if ($str[0] === 'u') { 60 | if (!isset($matches[2])) { 61 | throw new ShouldNotHappenException(); 62 | } 63 | return self::codePointToUtf8((int) hexdec($matches[2])); 64 | } 65 | 66 | return chr((int) octdec($str)); 67 | }, 68 | $str, 69 | ); 70 | } 71 | 72 | /** 73 | * Implementation based on https://github.com/nikic/PHP-Parser/blob/b0edd4c41111042d43bb45c6c657b2e0db367d9e/lib/PhpParser/Node/Scalar/String_.php#L132-L154 74 | */ 75 | private static function codePointToUtf8(int $num): string 76 | { 77 | if ($num <= 0x7F) { 78 | return chr($num); 79 | } 80 | if ($num <= 0x7FF) { 81 | return chr(($num >> 6) + 0xC0) 82 | . chr(($num & 0x3F) + 0x80); 83 | } 84 | if ($num <= 0xFFFF) { 85 | return chr(($num >> 12) + 0xE0) 86 | . chr((($num >> 6) & 0x3F) + 0x80) 87 | . chr(($num & 0x3F) + 0x80); 88 | } 89 | if ($num <= 0x1FFFFF) { 90 | return chr(($num >> 18) + 0xF0) 91 | . chr((($num >> 12) & 0x3F) + 0x80) 92 | . chr((($num >> 6) & 0x3F) + 0x80) 93 | . chr(($num & 0x3F) + 0x80); 94 | } 95 | 96 | // Invalid UTF-8 codepoint escape sequence: Codepoint too large 97 | return "\xef\xbf\xbd"; 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/Ast/ConstExpr/ConstExprStringNode.php: -------------------------------------------------------------------------------- 1 | value = $value; 35 | $this->quoteType = $quoteType; 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | if ($this->quoteType === self::SINGLE_QUOTED) { 41 | // from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1007 42 | return sprintf("'%s'", addcslashes($this->value, '\'\\')); 43 | } 44 | 45 | // from https://github.com/nikic/PHP-Parser/blob/0ffddce52d816f72d0efc4d9b02e276d3309ef01/lib/PhpParser/PrettyPrinter/Standard.php#L1010-L1040 46 | return sprintf('"%s"', $this->escapeDoubleQuotedString()); 47 | } 48 | 49 | private function escapeDoubleQuotedString(): string 50 | { 51 | $quote = '"'; 52 | $escaped = addcslashes($this->value, "\n\r\t\f\v$" . $quote . '\\'); 53 | 54 | // Escape control characters and non-UTF-8 characters. 55 | // Regex based on https://stackoverflow.com/a/11709412/385378. 56 | $regex = '/( 57 | [\x00-\x08\x0E-\x1F] # Control characters 58 | | [\xC0-\xC1] # Invalid UTF-8 Bytes 59 | | [\xF5-\xFF] # Invalid UTF-8 Bytes 60 | | \xE0(?=[\x80-\x9F]) # Overlong encoding of prior code point 61 | | \xF0(?=[\x80-\x8F]) # Overlong encoding of prior code point 62 | | [\xC2-\xDF](?![\x80-\xBF]) # Invalid UTF-8 Sequence Start 63 | | [\xE0-\xEF](?![\x80-\xBF]{2}) # Invalid UTF-8 Sequence Start 64 | | [\xF0-\xF4](?![\x80-\xBF]{3}) # Invalid UTF-8 Sequence Start 65 | | (?<=[\x00-\x7F\xF5-\xFF])[\x80-\xBF] # Invalid UTF-8 Sequence Middle 66 | | (?isEqual = $isEqual; 37 | } 38 | 39 | /** 40 | * Calculate diff (edit script) from $old to $new. 41 | * 42 | * @param T[] $old Original array 43 | * @param T[] $new New array 44 | * 45 | * @return DiffElem[] Diff (edit script) 46 | */ 47 | public function diff(array $old, array $new): array 48 | { 49 | [$trace, $x, $y] = $this->calculateTrace($old, $new); 50 | return $this->extractDiff($trace, $x, $y, $old, $new); 51 | } 52 | 53 | /** 54 | * Calculate diff, including "replace" operations. 55 | * 56 | * If a sequence of remove operations is followed by the same number of add operations, these 57 | * will be coalesced into replace operations. 58 | * 59 | * @param T[] $old Original array 60 | * @param T[] $new New array 61 | * 62 | * @return DiffElem[] Diff (edit script), including replace operations 63 | */ 64 | public function diffWithReplacements(array $old, array $new): array 65 | { 66 | return $this->coalesceReplacements($this->diff($old, $new)); 67 | } 68 | 69 | /** 70 | * @param T[] $old 71 | * @param T[] $new 72 | * @return array{array>, int, int} 73 | */ 74 | private function calculateTrace(array $old, array $new): array 75 | { 76 | $n = count($old); 77 | $m = count($new); 78 | $max = $n + $m; 79 | $v = [1 => 0]; 80 | $trace = []; 81 | for ($d = 0; $d <= $max; $d++) { 82 | $trace[] = $v; 83 | for ($k = -$d; $k <= $d; $k += 2) { 84 | if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) { 85 | $x = $v[$k + 1]; 86 | } else { 87 | $x = $v[$k - 1] + 1; 88 | } 89 | 90 | $y = $x - $k; 91 | while ($x < $n && $y < $m && ($this->isEqual)($old[$x], $new[$y])) { 92 | $x++; 93 | $y++; 94 | } 95 | 96 | $v[$k] = $x; 97 | if ($x >= $n && $y >= $m) { 98 | return [$trace, $x, $y]; 99 | } 100 | } 101 | } 102 | throw new Exception('Should not happen'); 103 | } 104 | 105 | /** 106 | * @param array> $trace 107 | * @param T[] $old 108 | * @param T[] $new 109 | * @return DiffElem[] 110 | */ 111 | private function extractDiff(array $trace, int $x, int $y, array $old, array $new): array 112 | { 113 | $result = []; 114 | for ($d = count($trace) - 1; $d >= 0; $d--) { 115 | $v = $trace[$d]; 116 | $k = $x - $y; 117 | 118 | if ($k === -$d || ($k !== $d && $v[$k - 1] < $v[$k + 1])) { 119 | $prevK = $k + 1; 120 | } else { 121 | $prevK = $k - 1; 122 | } 123 | 124 | $prevX = $v[$prevK]; 125 | $prevY = $prevX - $prevK; 126 | 127 | while ($x > $prevX && $y > $prevY) { 128 | $result[] = new DiffElem(DiffElem::TYPE_KEEP, $old[$x - 1], $new[$y - 1]); 129 | $x--; 130 | $y--; 131 | } 132 | 133 | if ($d === 0) { 134 | break; 135 | } 136 | 137 | while ($x > $prevX) { 138 | $result[] = new DiffElem(DiffElem::TYPE_REMOVE, $old[$x - 1], null); 139 | $x--; 140 | } 141 | 142 | while ($y > $prevY) { 143 | $result[] = new DiffElem(DiffElem::TYPE_ADD, null, $new[$y - 1]); 144 | $y--; 145 | } 146 | } 147 | return array_reverse($result); 148 | } 149 | 150 | /** 151 | * Coalesce equal-length sequences of remove+add into a replace operation. 152 | * 153 | * @param DiffElem[] $diff 154 | * @return DiffElem[] 155 | */ 156 | private function coalesceReplacements(array $diff): array 157 | { 158 | $newDiff = []; 159 | $c = count($diff); 160 | for ($i = 0; $i < $c; $i++) { 161 | $diffType = $diff[$i]->type; 162 | if ($diffType !== DiffElem::TYPE_REMOVE) { 163 | $newDiff[] = $diff[$i]; 164 | continue; 165 | } 166 | 167 | $j = $i; 168 | while ($j < $c && $diff[$j]->type === DiffElem::TYPE_REMOVE) { 169 | $j++; 170 | } 171 | 172 | $k = $j; 173 | while ($k < $c && $diff[$k]->type === DiffElem::TYPE_ADD) { 174 | $k++; 175 | } 176 | 177 | if ($j - $i === $k - $j) { 178 | $len = $j - $i; 179 | for ($n = 0; $n < $len; $n++) { 180 | $newDiff[] = new DiffElem( 181 | DiffElem::TYPE_REPLACE, 182 | $diff[$i + $n]->old, 183 | $diff[$j + $n]->new, 184 | ); 185 | } 186 | } else { 187 | for (; $i < $k; $i++) { 188 | $newDiff[] = $diff[$i]; 189 | } 190 | } 191 | $i = $k - 1; 192 | } 193 | return $newDiff; 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PHPDoc Parser for PHPStan

2 | 3 |

4 | Build Status 5 | Latest Stable Version 6 | License 7 | PHPStan Enabled 8 |

9 | 10 | This library `phpstan/phpdoc-parser` represents PHPDocs with an AST (Abstract Syntax Tree). It supports parsing and modifying PHPDocs. 11 | 12 | For the complete list of supported PHPDoc features check out PHPStan documentation. PHPStan is the main (but not the only) user of this library. 13 | 14 | * [PHPDoc Basics](https://phpstan.org/writing-php-code/phpdocs-basics) (list of PHPDoc tags) 15 | * [PHPDoc Types](https://phpstan.org/writing-php-code/phpdoc-types) (list of PHPDoc types) 16 | * [phpdoc-parser API Reference](https://phpstan.github.io/phpdoc-parser/2.3.x/namespace-PHPStan.PhpDocParser.html) with all the AST node types etc. 17 | 18 | This parser also supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/2.1.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). 19 | 20 | ## Installation 21 | 22 | ``` 23 | composer require phpstan/phpdoc-parser 24 | ``` 25 | 26 | ## Basic usage 27 | 28 | ```php 29 | tokenize('/** @param Lorem $a */')); 54 | $phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode 55 | $paramTags = $phpDocNode->getParamTagValues(); // ParamTagValueNode[] 56 | echo $paramTags[0]->parameterName; // '$a' 57 | echo $paramTags[0]->type; // IdentifierTypeNode - 'Lorem' 58 | ``` 59 | 60 | ### Format-preserving printer 61 | 62 | This component can be used to modify the AST 63 | and print it again as close as possible to the original. 64 | 65 | It's heavily inspired by format-preserving printer component in [nikic/PHP-Parser](https://github.com/nikic/PHP-Parser). 66 | 67 | ```php 68 | true, 'indexes' => true, 'comments' => true]); 87 | $lexer = new Lexer($config); 88 | $constExprParser = new ConstExprParser($config); 89 | $typeParser = new TypeParser($config, $constExprParser); 90 | $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); 91 | 92 | $tokens = new TokenIterator($lexer->tokenize('/** @param Lorem $a */')); 93 | $phpDocNode = $phpDocParser->parse($tokens); // PhpDocNode 94 | 95 | $cloningTraverser = new NodeTraverser([new CloningVisitor()]); 96 | 97 | /** @var PhpDocNode $newPhpDocNode */ 98 | [$newPhpDocNode] = $cloningTraverser->traverse([$phpDocNode]); 99 | 100 | // change something in $newPhpDocNode 101 | $newPhpDocNode->getParamTagValues()[0]->type = new IdentifierTypeNode('Ipsum'); 102 | 103 | // print changed PHPDoc 104 | $printer = new Printer(); 105 | $newPhpDoc = $printer->printFormatPreserving($newPhpDocNode, $phpDocNode, $tokens); 106 | echo $newPhpDoc; // '/** @param Ipsum $a */' 107 | ``` 108 | 109 | ## Code of Conduct 110 | 111 | This project adheres to a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project and its community, you are expected to uphold this code. 112 | 113 | ## Building 114 | 115 | Initially you need to run `composer install`, or `composer update` in case you aren't working in a folder which was built before. 116 | 117 | Afterwards you can either run the whole build including linting and coding standards using 118 | 119 | make 120 | 121 | or run only tests using 122 | 123 | make tests 124 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | Upgrading from phpstan/phpdoc-parser 1.x to 2.0 2 | ================================= 3 | 4 | ### PHP version requirements 5 | 6 | phpstan/phpdoc-parser now requires PHP 7.4 or newer to run. 7 | 8 | ### Changed constructors of parser classes 9 | 10 | Instead of different arrays and boolean values passed into class constructors during setup, parser classes now share a common ParserConfig object. 11 | 12 | Before: 13 | 14 | ```php 15 | use PHPStan\PhpDocParser\Lexer\Lexer; 16 | use PHPStan\PhpDocParser\Parser\ConstExprParser; 17 | use PHPStan\PhpDocParser\Parser\TypeParser; 18 | use PHPStan\PhpDocParser\Parser\PhpDocParser; 19 | 20 | $usedAttributes = ['lines' => true, 'indexes' => true]; 21 | 22 | $lexer = new Lexer(); 23 | $constExprParser = new ConstExprParser(true, true, $usedAttributes); 24 | $typeParser = new TypeParser($constExprParser, true, $usedAttributes); 25 | $phpDocParser = new PhpDocParser($typeParser, $constExprParser, true, true, $usedAttributes); 26 | ``` 27 | 28 | After: 29 | 30 | ```php 31 | use PHPStan\PhpDocParser\Lexer\Lexer; 32 | use PHPStan\PhpDocParser\ParserConfig; 33 | use PHPStan\PhpDocParser\Parser\ConstExprParser; 34 | use PHPStan\PhpDocParser\Parser\TypeParser; 35 | use PHPStan\PhpDocParser\Parser\PhpDocParser; 36 | 37 | $config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true]); 38 | $lexer = new Lexer($config); 39 | $constExprParser = new ConstExprParser($config); 40 | $typeParser = new TypeParser($config, $constExprParser); 41 | $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); 42 | ``` 43 | 44 | The point of ParserConfig is that over the course of phpstan/phpdoc-parser 2.x development series it's most likely going to gain new optional parameters akin to PHPStan's [bleeding edge](https://phpstan.org/blog/what-is-bleeding-edge). These parameters will allow opting in to new behaviour which will become the default in 3.0. 45 | 46 | With ParserConfig object, it's now going to be impossible to configure parser classes inconsistently. Which [happened to users](https://github.com/phpstan/phpdoc-parser/issues/251#issuecomment-2333927959) when they were separate boolean values. 47 | 48 | ### Support for parsing Doctrine annotations 49 | 50 | This parser now supports parsing [Doctrine Annotations](https://github.com/doctrine/annotations). The AST nodes representing Doctrine Annotations live in the [PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine namespace](https://phpstan.github.io/phpdoc-parser/2.0.x/namespace-PHPStan.PhpDocParser.Ast.PhpDoc.Doctrine.html). 51 | 52 | ### Whitespace before description is required 53 | 54 | phpdoc-parser 1.x sometimes silently consumed invalid part of a PHPDoc type as description: 55 | 56 | ```php 57 | /** @return \Closure(...int, string): string */ 58 | ``` 59 | 60 | This became `IdentifierTypeNode` of `\Closure` and with `(...int, string): string` as description. (Valid callable syntax is: `\Closure(int ...$u, string): string`.) 61 | 62 | Another example: 63 | 64 | ```php 65 | /** @return array{foo: int}} */ 66 | ``` 67 | 68 | The extra `}` also became description. 69 | 70 | Both of these examples are now InvalidTagValueNode. 71 | 72 | If these parts are supposed to be PHPDoc descriptions, you need to put whitespace between the type and the description text: 73 | 74 | ```php 75 | /** @return \Closure (...int, string): string */ 76 | /** @return array{foo: int} } */ 77 | ``` 78 | 79 | ### Type aliases with invalid types are preserved 80 | 81 | In phpdoc-parser 1.x, invalid type alias syntax was represented as [`InvalidTagValueNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.InvalidTagValueNode.html), losing information about a type alias being present. 82 | 83 | ```php 84 | /** 85 | * @phpstan-type TypeAlias 86 | */ 87 | ``` 88 | 89 | This `@phpstan-type` is missing the actual type to alias. In phpdoc-parser 2.0 this is now represented as [`TypeAliasTagValueNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.TypeAliasTagValueNode.html) (instead of `InvalidTagValueNode`) with [`InvalidTypeNode`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.Type.InvalidTypeNode.html) in place of the type. 90 | 91 | ### Removal of QuoteAwareConstExprStringNode 92 | 93 | The class [QuoteAwareConstExprStringNode](https://phpstan.github.io/phpdoc-parser/1.23.x/PHPStan.PhpDocParser.Ast.ConstExpr.QuoteAwareConstExprStringNode.html) has been removed. 94 | 95 | Instead, [ConstExprStringNode](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.ConstExpr.ConstExprStringNode.html) gained information about the kind of quotes being used. 96 | 97 | ### Removed 2nd parameter of `ConstExprParser::parse()` (`$trimStrings`) 98 | 99 | `ConstExprStringNode::$value` now contains unescaped values without surrounding `''` or `""` quotes. 100 | 101 | Use `ConstExprStringNode::__toString()` or [`Printer`](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Printer.Printer.html) to get the escaped value along with surrounding quotes. 102 | 103 | ### Text between tags always belongs to description 104 | 105 | Multi-line descriptions between tags were previously represented as separate [PhpDocTextNode](https://phpstan.github.io/phpdoc-parser/2.0.x/PHPStan.PhpDocParser.Ast.PhpDoc.PhpDocTextNode.html): 106 | 107 | ```php 108 | /** 109 | * @param Foo $foo 1st multi world description 110 | * some text in the middle 111 | * @param Bar $bar 2nd multi world description 112 | */ 113 | ``` 114 | 115 | The line with `some text in the middle` in phpdoc-parser 2.0 is now part of the description of the first `@param` tag. 116 | 117 | ### `ArrayShapeNode` construction changes 118 | 119 | `ArrayShapeNode` constructor made private, added public static methods `createSealed()` and `createUnsealed()`. 120 | 121 | ### Minor BC breaks 122 | 123 | * Constructor parameter `$isEquality` in `AssertTag*ValueNode` made required 124 | * Constructor parameter `$templateTypes` in `MethodTagValueNode` made required 125 | * Constructor parameter `$isReference` in `ParamTagValueNode` made required 126 | * Constructor parameter `$isReference` in `TypelessParamTagValueNode` made required 127 | * Constructor parameter `$templateTypes` in `CallableTypeNode` made required 128 | * Constructor parameters `$expectedTokenValue` and `$currentTokenLine` in `ParserException` made required 129 | * `ArrayShapeItemNode` and `ObjectShapeItemNode` are not standalone TypeNode, just Node 130 | -------------------------------------------------------------------------------- /src/Lexer/Lexer.php: -------------------------------------------------------------------------------- 1 | '\'&\'', 58 | self::TOKEN_UNION => '\'|\'', 59 | self::TOKEN_INTERSECTION => '\'&\'', 60 | self::TOKEN_NULLABLE => '\'?\'', 61 | self::TOKEN_NEGATED => '\'!\'', 62 | self::TOKEN_OPEN_PARENTHESES => '\'(\'', 63 | self::TOKEN_CLOSE_PARENTHESES => '\')\'', 64 | self::TOKEN_OPEN_ANGLE_BRACKET => '\'<\'', 65 | self::TOKEN_CLOSE_ANGLE_BRACKET => '\'>\'', 66 | self::TOKEN_OPEN_SQUARE_BRACKET => '\'[\'', 67 | self::TOKEN_CLOSE_SQUARE_BRACKET => '\']\'', 68 | self::TOKEN_OPEN_CURLY_BRACKET => '\'{\'', 69 | self::TOKEN_CLOSE_CURLY_BRACKET => '\'}\'', 70 | self::TOKEN_COMMA => '\',\'', 71 | self::TOKEN_COMMENT => '\'//\'', 72 | self::TOKEN_COLON => '\':\'', 73 | self::TOKEN_VARIADIC => '\'...\'', 74 | self::TOKEN_DOUBLE_COLON => '\'::\'', 75 | self::TOKEN_DOUBLE_ARROW => '\'=>\'', 76 | self::TOKEN_ARROW => '\'->\'', 77 | self::TOKEN_EQUAL => '\'=\'', 78 | self::TOKEN_OPEN_PHPDOC => '\'/**\'', 79 | self::TOKEN_CLOSE_PHPDOC => '\'*/\'', 80 | self::TOKEN_PHPDOC_TAG => 'TOKEN_PHPDOC_TAG', 81 | self::TOKEN_DOCTRINE_TAG => 'TOKEN_DOCTRINE_TAG', 82 | self::TOKEN_PHPDOC_EOL => 'TOKEN_PHPDOC_EOL', 83 | self::TOKEN_FLOAT => 'TOKEN_FLOAT', 84 | self::TOKEN_INTEGER => 'TOKEN_INTEGER', 85 | self::TOKEN_SINGLE_QUOTED_STRING => 'TOKEN_SINGLE_QUOTED_STRING', 86 | self::TOKEN_DOUBLE_QUOTED_STRING => 'TOKEN_DOUBLE_QUOTED_STRING', 87 | self::TOKEN_DOCTRINE_ANNOTATION_STRING => 'TOKEN_DOCTRINE_ANNOTATION_STRING', 88 | self::TOKEN_IDENTIFIER => 'type', 89 | self::TOKEN_THIS_VARIABLE => '\'$this\'', 90 | self::TOKEN_VARIABLE => 'variable', 91 | self::TOKEN_HORIZONTAL_WS => 'TOKEN_HORIZONTAL_WS', 92 | self::TOKEN_OTHER => 'TOKEN_OTHER', 93 | self::TOKEN_END => 'TOKEN_END', 94 | self::TOKEN_WILDCARD => '*', 95 | ]; 96 | 97 | public const VALUE_OFFSET = 0; 98 | public const TYPE_OFFSET = 1; 99 | public const LINE_OFFSET = 2; 100 | 101 | private ParserConfig $config; // @phpstan-ignore property.onlyWritten 102 | 103 | private ?string $regexp = null; 104 | 105 | public function __construct(ParserConfig $config) 106 | { 107 | $this->config = $config; 108 | } 109 | 110 | /** 111 | * @return list 112 | */ 113 | public function tokenize(string $s): array 114 | { 115 | if ($this->regexp === null) { 116 | $this->regexp = $this->generateRegexp(); 117 | } 118 | 119 | preg_match_all($this->regexp, $s, $matches, PREG_SET_ORDER); 120 | 121 | $tokens = []; 122 | $line = 1; 123 | foreach ($matches as $match) { 124 | $type = (int) $match['MARK']; 125 | $tokens[] = [$match[0], $type, $line]; 126 | if ($type !== self::TOKEN_PHPDOC_EOL) { 127 | continue; 128 | } 129 | 130 | $line++; 131 | } 132 | 133 | $tokens[] = ['', self::TOKEN_END, $line]; 134 | 135 | return $tokens; 136 | } 137 | 138 | private function generateRegexp(): string 139 | { 140 | $patterns = [ 141 | self::TOKEN_HORIZONTAL_WS => '[\\x09\\x20]++', 142 | 143 | self::TOKEN_IDENTIFIER => '(?:[\\\\]?+[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF-]*+)++', 144 | self::TOKEN_THIS_VARIABLE => '\\$this(?![0-9a-z_\\x80-\\xFF])', 145 | self::TOKEN_VARIABLE => '\\$[a-z_\\x80-\\xFF][0-9a-z_\\x80-\\xFF]*+', 146 | 147 | // '&' followed by TOKEN_VARIADIC, TOKEN_VARIABLE, TOKEN_EQUAL, TOKEN_EQUAL or TOKEN_CLOSE_PARENTHESES 148 | self::TOKEN_REFERENCE => '&(?=\\s*+(?:[.,=)]|(?:\\$(?!this(?![0-9a-z_\\x80-\\xFF])))))', 149 | self::TOKEN_UNION => '\\|', 150 | self::TOKEN_INTERSECTION => '&', 151 | self::TOKEN_NULLABLE => '\\?', 152 | self::TOKEN_NEGATED => '!', 153 | 154 | self::TOKEN_OPEN_PARENTHESES => '\\(', 155 | self::TOKEN_CLOSE_PARENTHESES => '\\)', 156 | self::TOKEN_OPEN_ANGLE_BRACKET => '<', 157 | self::TOKEN_CLOSE_ANGLE_BRACKET => '>', 158 | self::TOKEN_OPEN_SQUARE_BRACKET => '\\[', 159 | self::TOKEN_CLOSE_SQUARE_BRACKET => '\\]', 160 | self::TOKEN_OPEN_CURLY_BRACKET => '\\{', 161 | self::TOKEN_CLOSE_CURLY_BRACKET => '\\}', 162 | 163 | self::TOKEN_COMMA => ',', 164 | self::TOKEN_COMMENT => '\/\/[^\\r\\n]*(?=\n|\r|\*/)', 165 | self::TOKEN_VARIADIC => '\\.\\.\\.', 166 | self::TOKEN_DOUBLE_COLON => '::', 167 | self::TOKEN_DOUBLE_ARROW => '=>', 168 | self::TOKEN_ARROW => '->', 169 | self::TOKEN_EQUAL => '=', 170 | self::TOKEN_COLON => ':', 171 | 172 | self::TOKEN_OPEN_PHPDOC => '/\\*\\*(?=\\s)\\x20?+', 173 | self::TOKEN_CLOSE_PHPDOC => '\\*/', 174 | self::TOKEN_PHPDOC_TAG => '@(?:[a-z][a-z0-9-\\\\]+:)?[a-z][a-z0-9-\\\\]*+', 175 | self::TOKEN_DOCTRINE_TAG => '@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*', 176 | self::TOKEN_PHPDOC_EOL => '\\r?+\\n[\\x09\\x20]*+(?:\\*(?!/)\\x20?+)?', 177 | 178 | self::TOKEN_FLOAT => '[+\-]?(?:(?:[0-9]++(_[0-9]++)*\\.[0-9]*+(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]*+(_[0-9]++)*\\.[0-9]++(_[0-9]++)*(?:e[+\-]?[0-9]++(_[0-9]++)*)?)|(?:[0-9]++(_[0-9]++)*e[+\-]?[0-9]++(_[0-9]++)*))', 179 | self::TOKEN_INTEGER => '[+\-]?(?:(?:0b[0-1]++(_[0-1]++)*)|(?:0o[0-7]++(_[0-7]++)*)|(?:0x[0-9a-f]++(_[0-9a-f]++)*)|(?:[0-9]++(_[0-9]++)*))', 180 | self::TOKEN_SINGLE_QUOTED_STRING => '\'(?:\\\\[^\\r\\n]|[^\'\\r\\n\\\\])*+\'', 181 | self::TOKEN_DOUBLE_QUOTED_STRING => '"(?:\\\\[^\\r\\n]|[^"\\r\\n\\\\])*+"', 182 | self::TOKEN_DOCTRINE_ANNOTATION_STRING => '"(?:""|[^"])*+"', 183 | 184 | self::TOKEN_WILDCARD => '\\*', 185 | 186 | // anything but TOKEN_CLOSE_PHPDOC or TOKEN_HORIZONTAL_WS or TOKEN_EOL 187 | self::TOKEN_OTHER => '(?:(?!\\*/)[^\\s])++', 188 | ]; 189 | 190 | foreach ($patterns as $type => &$pattern) { 191 | $pattern = '(?:' . $pattern . ')(*MARK:' . $type . ')'; 192 | } 193 | 194 | return '~' . implode('|', $patterns) . '~Asi'; 195 | } 196 | 197 | } 198 | -------------------------------------------------------------------------------- /src/Parser/ConstExprParser.php: -------------------------------------------------------------------------------- 1 | config = $config; 23 | $this->parseDoctrineStrings = false; 24 | } 25 | 26 | /** 27 | * @internal 28 | */ 29 | public function toDoctrine(): self 30 | { 31 | $self = new self($this->config); 32 | $self->parseDoctrineStrings = true; 33 | return $self; 34 | } 35 | 36 | public function parse(TokenIterator $tokens): Ast\ConstExpr\ConstExprNode 37 | { 38 | $startLine = $tokens->currentTokenLine(); 39 | $startIndex = $tokens->currentTokenIndex(); 40 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_FLOAT)) { 41 | $value = $tokens->currentTokenValue(); 42 | $tokens->next(); 43 | 44 | return $this->enrichWithAttributes( 45 | $tokens, 46 | new Ast\ConstExpr\ConstExprFloatNode(str_replace('_', '', $value)), 47 | $startLine, 48 | $startIndex, 49 | ); 50 | } 51 | 52 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { 53 | $value = $tokens->currentTokenValue(); 54 | $tokens->next(); 55 | 56 | return $this->enrichWithAttributes( 57 | $tokens, 58 | new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $value)), 59 | $startLine, 60 | $startIndex, 61 | ); 62 | } 63 | 64 | if ($this->parseDoctrineStrings && $tokens->isCurrentTokenType(Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) { 65 | $value = $tokens->currentTokenValue(); 66 | $tokens->next(); 67 | 68 | return $this->enrichWithAttributes( 69 | $tokens, 70 | new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($value)), 71 | $startLine, 72 | $startIndex, 73 | ); 74 | } 75 | 76 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING, Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { 77 | if ($this->parseDoctrineStrings) { 78 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { 79 | throw new ParserException( 80 | $tokens->currentTokenValue(), 81 | $tokens->currentTokenType(), 82 | $tokens->currentTokenOffset(), 83 | Lexer::TOKEN_DOUBLE_QUOTED_STRING, 84 | null, 85 | $tokens->currentTokenLine(), 86 | ); 87 | } 88 | 89 | $value = $tokens->currentTokenValue(); 90 | $tokens->next(); 91 | 92 | return $this->enrichWithAttributes( 93 | $tokens, 94 | $this->parseDoctrineString($value, $tokens), 95 | $startLine, 96 | $startIndex, 97 | ); 98 | } 99 | 100 | $value = StringUnescaper::unescapeString($tokens->currentTokenValue()); 101 | $type = $tokens->currentTokenType(); 102 | $tokens->next(); 103 | 104 | return $this->enrichWithAttributes( 105 | $tokens, 106 | new Ast\ConstExpr\ConstExprStringNode( 107 | $value, 108 | $type === Lexer::TOKEN_SINGLE_QUOTED_STRING 109 | ? Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED 110 | : Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED, 111 | ), 112 | $startLine, 113 | $startIndex, 114 | ); 115 | 116 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { 117 | $identifier = $tokens->currentTokenValue(); 118 | $tokens->next(); 119 | 120 | switch (strtolower($identifier)) { 121 | case 'true': 122 | return $this->enrichWithAttributes( 123 | $tokens, 124 | new Ast\ConstExpr\ConstExprTrueNode(), 125 | $startLine, 126 | $startIndex, 127 | ); 128 | case 'false': 129 | return $this->enrichWithAttributes( 130 | $tokens, 131 | new Ast\ConstExpr\ConstExprFalseNode(), 132 | $startLine, 133 | $startIndex, 134 | ); 135 | case 'null': 136 | return $this->enrichWithAttributes( 137 | $tokens, 138 | new Ast\ConstExpr\ConstExprNullNode(), 139 | $startLine, 140 | $startIndex, 141 | ); 142 | case 'array': 143 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); 144 | return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_PARENTHESES, $startIndex); 145 | } 146 | 147 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) { 148 | $classConstantName = ''; 149 | $lastType = null; 150 | while (true) { 151 | if ($lastType !== Lexer::TOKEN_IDENTIFIER && $tokens->currentTokenType() === Lexer::TOKEN_IDENTIFIER) { 152 | $classConstantName .= $tokens->currentTokenValue(); 153 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 154 | $lastType = Lexer::TOKEN_IDENTIFIER; 155 | 156 | continue; 157 | } 158 | 159 | if ($lastType !== Lexer::TOKEN_WILDCARD && $tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { 160 | $classConstantName .= '*'; 161 | $lastType = Lexer::TOKEN_WILDCARD; 162 | 163 | if ($tokens->getSkippedHorizontalWhiteSpaceIfAny() !== '') { 164 | break; 165 | } 166 | 167 | continue; 168 | } 169 | 170 | if ($lastType === null) { 171 | // trigger parse error if nothing valid was consumed 172 | $tokens->consumeTokenType(Lexer::TOKEN_WILDCARD); 173 | } 174 | 175 | break; 176 | } 177 | 178 | return $this->enrichWithAttributes( 179 | $tokens, 180 | new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName), 181 | $startLine, 182 | $startIndex, 183 | ); 184 | 185 | } 186 | 187 | return $this->enrichWithAttributes( 188 | $tokens, 189 | new Ast\ConstExpr\ConstFetchNode('', $identifier), 190 | $startLine, 191 | $startIndex, 192 | ); 193 | 194 | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 195 | return $this->parseArray($tokens, Lexer::TOKEN_CLOSE_SQUARE_BRACKET, $startIndex); 196 | } 197 | 198 | throw new ParserException( 199 | $tokens->currentTokenValue(), 200 | $tokens->currentTokenType(), 201 | $tokens->currentTokenOffset(), 202 | Lexer::TOKEN_IDENTIFIER, 203 | null, 204 | $tokens->currentTokenLine(), 205 | ); 206 | } 207 | 208 | private function parseArray(TokenIterator $tokens, int $endToken, int $startIndex): Ast\ConstExpr\ConstExprArrayNode 209 | { 210 | $items = []; 211 | 212 | $startLine = $tokens->currentTokenLine(); 213 | 214 | if (!$tokens->tryConsumeTokenType($endToken)) { 215 | do { 216 | $items[] = $this->parseArrayItem($tokens); 217 | } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) && !$tokens->isCurrentTokenType($endToken)); 218 | $tokens->consumeTokenType($endToken); 219 | } 220 | 221 | return $this->enrichWithAttributes( 222 | $tokens, 223 | new Ast\ConstExpr\ConstExprArrayNode($items), 224 | $startLine, 225 | $startIndex, 226 | ); 227 | } 228 | 229 | /** 230 | * This method is supposed to be called with TokenIterator after reading TOKEN_DOUBLE_QUOTED_STRING and shifting 231 | * to the next token. 232 | */ 233 | public function parseDoctrineString(string $text, TokenIterator $tokens): Ast\ConstExpr\DoctrineConstExprStringNode 234 | { 235 | // Because of how Lexer works, a valid Doctrine string 236 | // can consist of a sequence of TOKEN_DOUBLE_QUOTED_STRING and TOKEN_DOCTRINE_ANNOTATION_STRING 237 | while ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING, Lexer::TOKEN_DOCTRINE_ANNOTATION_STRING)) { 238 | $text .= $tokens->currentTokenValue(); 239 | $tokens->next(); 240 | } 241 | 242 | return new Ast\ConstExpr\DoctrineConstExprStringNode(Ast\ConstExpr\DoctrineConstExprStringNode::unescape($text)); 243 | } 244 | 245 | private function parseArrayItem(TokenIterator $tokens): Ast\ConstExpr\ConstExprArrayItemNode 246 | { 247 | $startLine = $tokens->currentTokenLine(); 248 | $startIndex = $tokens->currentTokenIndex(); 249 | 250 | $expr = $this->parse($tokens); 251 | 252 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_ARROW)) { 253 | $key = $expr; 254 | $value = $this->parse($tokens); 255 | 256 | } else { 257 | $key = null; 258 | $value = $expr; 259 | } 260 | 261 | return $this->enrichWithAttributes( 262 | $tokens, 263 | new Ast\ConstExpr\ConstExprArrayItemNode($key, $value), 264 | $startLine, 265 | $startIndex, 266 | ); 267 | } 268 | 269 | /** 270 | * @template T of Ast\ConstExpr\ConstExprNode 271 | * @param T $node 272 | * @return T 273 | */ 274 | private function enrichWithAttributes(TokenIterator $tokens, Ast\ConstExpr\ConstExprNode $node, int $startLine, int $startIndex): Ast\ConstExpr\ConstExprNode 275 | { 276 | if ($this->config->useLinesAttributes) { 277 | $node->setAttribute(Ast\Attribute::START_LINE, $startLine); 278 | $node->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine()); 279 | } 280 | 281 | if ($this->config->useIndexAttributes) { 282 | $node->setAttribute(Ast\Attribute::START_INDEX, $startIndex); 283 | $node->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken()); 284 | } 285 | 286 | return $node; 287 | } 288 | 289 | } 290 | -------------------------------------------------------------------------------- /src/Ast/NodeTraverser.php: -------------------------------------------------------------------------------- 1 | Visitors */ 65 | private array $visitors = []; 66 | 67 | /** @var bool Whether traversal should be stopped */ 68 | private bool $stopTraversal; 69 | 70 | /** 71 | * @param list $visitors 72 | */ 73 | public function __construct(array $visitors) 74 | { 75 | $this->visitors = $visitors; 76 | } 77 | 78 | /** 79 | * Traverses an array of nodes using the registered visitors. 80 | * 81 | * @param Node[] $nodes Array of nodes 82 | * 83 | * @return Node[] Traversed array of nodes 84 | */ 85 | public function traverse(array $nodes): array 86 | { 87 | $this->stopTraversal = false; 88 | 89 | foreach ($this->visitors as $visitor) { 90 | $return = $visitor->beforeTraverse($nodes); 91 | if ($return === null) { 92 | continue; 93 | } 94 | 95 | $nodes = $return; 96 | } 97 | 98 | $nodes = $this->traverseArray($nodes); 99 | 100 | foreach ($this->visitors as $visitor) { 101 | $return = $visitor->afterTraverse($nodes); 102 | if ($return === null) { 103 | continue; 104 | } 105 | 106 | $nodes = $return; 107 | } 108 | 109 | return $nodes; 110 | } 111 | 112 | /** 113 | * Recursively traverse a node. 114 | * 115 | * @param Node $node Node to traverse. 116 | * 117 | * @return Node Result of traversal (may be original node or new one) 118 | */ 119 | private function traverseNode(Node $node): Node 120 | { 121 | $subNodeNames = array_keys(get_object_vars($node)); 122 | foreach ($subNodeNames as $name) { 123 | $subNode =& $node->$name; 124 | 125 | if (is_array($subNode)) { 126 | $subNode = $this->traverseArray($subNode); 127 | if ($this->stopTraversal) { 128 | break; 129 | } 130 | } elseif ($subNode instanceof Node) { 131 | $traverseChildren = true; 132 | $breakVisitorIndex = null; 133 | 134 | foreach ($this->visitors as $visitorIndex => $visitor) { 135 | $return = $visitor->enterNode($subNode); 136 | if ($return === null) { 137 | continue; 138 | } 139 | 140 | if ($return instanceof Node) { 141 | $this->ensureReplacementReasonable($subNode, $return); 142 | $subNode = $return; 143 | } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { 144 | $traverseChildren = false; 145 | } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { 146 | $traverseChildren = false; 147 | $breakVisitorIndex = $visitorIndex; 148 | break; 149 | } elseif ($return === self::STOP_TRAVERSAL) { 150 | $this->stopTraversal = true; 151 | break 2; 152 | } else { 153 | throw new LogicException( 154 | 'enterNode() returned invalid value of type ' . gettype($return), 155 | ); 156 | } 157 | } 158 | 159 | if ($traverseChildren) { 160 | $subNode = $this->traverseNode($subNode); 161 | if ($this->stopTraversal) { 162 | break; 163 | } 164 | } 165 | 166 | foreach ($this->visitors as $visitorIndex => $visitor) { 167 | $return = $visitor->leaveNode($subNode); 168 | 169 | if ($return !== null) { 170 | if ($return instanceof Node) { 171 | $this->ensureReplacementReasonable($subNode, $return); 172 | $subNode = $return; 173 | } elseif ($return === self::STOP_TRAVERSAL) { 174 | $this->stopTraversal = true; 175 | break 2; 176 | } elseif (is_array($return)) { 177 | throw new LogicException( 178 | 'leaveNode() may only return an array ' . 179 | 'if the parent structure is an array', 180 | ); 181 | } else { 182 | throw new LogicException( 183 | 'leaveNode() returned invalid value of type ' . gettype($return), 184 | ); 185 | } 186 | } 187 | 188 | if ($breakVisitorIndex === $visitorIndex) { 189 | break; 190 | } 191 | } 192 | } 193 | } 194 | 195 | return $node; 196 | } 197 | 198 | /** 199 | * Recursively traverse array (usually of nodes). 200 | * 201 | * @param mixed[] $nodes Array to traverse 202 | * 203 | * @return mixed[] Result of traversal (may be original array or changed one) 204 | */ 205 | private function traverseArray(array $nodes): array 206 | { 207 | $doNodes = []; 208 | 209 | foreach ($nodes as $i => &$node) { 210 | if ($node instanceof Node) { 211 | $traverseChildren = true; 212 | $breakVisitorIndex = null; 213 | 214 | foreach ($this->visitors as $visitorIndex => $visitor) { 215 | $return = $visitor->enterNode($node); 216 | if ($return === null) { 217 | continue; 218 | } 219 | 220 | if ($return instanceof Node) { 221 | $this->ensureReplacementReasonable($node, $return); 222 | $node = $return; 223 | } elseif (is_array($return)) { 224 | $doNodes[] = [$i, $return]; 225 | continue 2; 226 | } elseif ($return === self::REMOVE_NODE) { 227 | $doNodes[] = [$i, []]; 228 | continue 2; 229 | } elseif ($return === self::DONT_TRAVERSE_CHILDREN) { 230 | $traverseChildren = false; 231 | } elseif ($return === self::DONT_TRAVERSE_CURRENT_AND_CHILDREN) { 232 | $traverseChildren = false; 233 | $breakVisitorIndex = $visitorIndex; 234 | break; 235 | } elseif ($return === self::STOP_TRAVERSAL) { 236 | $this->stopTraversal = true; 237 | break 2; 238 | } else { 239 | throw new LogicException( 240 | 'enterNode() returned invalid value of type ' . gettype($return), 241 | ); 242 | } 243 | } 244 | 245 | if ($traverseChildren) { 246 | $node = $this->traverseNode($node); 247 | if ($this->stopTraversal) { 248 | break; 249 | } 250 | } 251 | 252 | foreach ($this->visitors as $visitorIndex => $visitor) { 253 | $return = $visitor->leaveNode($node); 254 | 255 | if ($return !== null) { 256 | if ($return instanceof Node) { 257 | $this->ensureReplacementReasonable($node, $return); 258 | $node = $return; 259 | } elseif (is_array($return)) { 260 | $doNodes[] = [$i, $return]; 261 | break; 262 | } elseif ($return === self::REMOVE_NODE) { 263 | $doNodes[] = [$i, []]; 264 | break; 265 | } elseif ($return === self::STOP_TRAVERSAL) { 266 | $this->stopTraversal = true; 267 | break 2; 268 | } else { 269 | throw new LogicException( 270 | 'leaveNode() returned invalid value of type ' . gettype($return), 271 | ); 272 | } 273 | } 274 | 275 | if ($breakVisitorIndex === $visitorIndex) { 276 | break; 277 | } 278 | } 279 | } elseif (is_array($node)) { 280 | throw new LogicException('Invalid node structure: Contains nested arrays'); 281 | } 282 | } 283 | 284 | if (count($doNodes) > 0) { 285 | while ([$i, $replace] = array_pop($doNodes)) { 286 | array_splice($nodes, $i, 1, $replace); 287 | } 288 | } 289 | 290 | return $nodes; 291 | } 292 | 293 | private function ensureReplacementReasonable(Node $old, Node $new): void 294 | { 295 | if ($old instanceof TypeNode && !$new instanceof TypeNode) { 296 | throw new LogicException(sprintf('Trying to replace TypeNode with %s', get_class($new))); 297 | } 298 | 299 | if ($old instanceof ConstExprNode && !$new instanceof ConstExprNode) { 300 | throw new LogicException(sprintf('Trying to replace ConstExprNode with %s', get_class($new))); 301 | } 302 | 303 | if ($old instanceof PhpDocChildNode && !$new instanceof PhpDocChildNode) { 304 | throw new LogicException(sprintf('Trying to replace PhpDocChildNode with %s', get_class($new))); 305 | } 306 | 307 | if ($old instanceof PhpDocTagValueNode && !$new instanceof PhpDocTagValueNode) { 308 | throw new LogicException(sprintf('Trying to replace PhpDocTagValueNode with %s', get_class($new))); 309 | } 310 | } 311 | 312 | } 313 | -------------------------------------------------------------------------------- /src/Parser/TokenIterator.php: -------------------------------------------------------------------------------- 1 | */ 19 | private array $tokens; 20 | 21 | private int $index; 22 | 23 | /** @var list */ 24 | private array $comments = []; 25 | 26 | /** @var list}> */ 27 | private array $savePoints = []; 28 | 29 | /** @var list */ 30 | private array $skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS]; 31 | 32 | private ?string $newline = null; 33 | 34 | /** 35 | * @param list $tokens 36 | */ 37 | public function __construct(array $tokens, int $index = 0) 38 | { 39 | $this->tokens = $tokens; 40 | $this->index = $index; 41 | 42 | $this->skipIrrelevantTokens(); 43 | } 44 | 45 | /** 46 | * @return list 47 | */ 48 | public function getTokens(): array 49 | { 50 | return $this->tokens; 51 | } 52 | 53 | public function getContentBetween(int $startPos, int $endPos): string 54 | { 55 | if ($startPos < 0 || $endPos > count($this->tokens)) { 56 | throw new LogicException(); 57 | } 58 | 59 | $content = ''; 60 | for ($i = $startPos; $i < $endPos; $i++) { 61 | $content .= $this->tokens[$i][Lexer::VALUE_OFFSET]; 62 | } 63 | 64 | return $content; 65 | } 66 | 67 | public function getTokenCount(): int 68 | { 69 | return count($this->tokens); 70 | } 71 | 72 | public function currentTokenValue(): string 73 | { 74 | return $this->tokens[$this->index][Lexer::VALUE_OFFSET]; 75 | } 76 | 77 | public function currentTokenType(): int 78 | { 79 | return $this->tokens[$this->index][Lexer::TYPE_OFFSET]; 80 | } 81 | 82 | public function currentTokenOffset(): int 83 | { 84 | $offset = 0; 85 | for ($i = 0; $i < $this->index; $i++) { 86 | $offset += strlen($this->tokens[$i][Lexer::VALUE_OFFSET]); 87 | } 88 | 89 | return $offset; 90 | } 91 | 92 | public function currentTokenLine(): int 93 | { 94 | return $this->tokens[$this->index][Lexer::LINE_OFFSET]; 95 | } 96 | 97 | public function currentTokenIndex(): int 98 | { 99 | return $this->index; 100 | } 101 | 102 | public function endIndexOfLastRelevantToken(): int 103 | { 104 | $endIndex = $this->currentTokenIndex(); 105 | $endIndex--; 106 | while (in_array($this->tokens[$endIndex][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) { 107 | if (!isset($this->tokens[$endIndex - 1])) { 108 | break; 109 | } 110 | $endIndex--; 111 | } 112 | 113 | return $endIndex; 114 | } 115 | 116 | public function isCurrentTokenValue(string $tokenValue): bool 117 | { 118 | return $this->tokens[$this->index][Lexer::VALUE_OFFSET] === $tokenValue; 119 | } 120 | 121 | public function isCurrentTokenType(int ...$tokenType): bool 122 | { 123 | return in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true); 124 | } 125 | 126 | public function isPrecededByHorizontalWhitespace(): bool 127 | { 128 | return ($this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] ?? -1) === Lexer::TOKEN_HORIZONTAL_WS; 129 | } 130 | 131 | /** 132 | * @throws ParserException 133 | */ 134 | public function consumeTokenType(int $tokenType): void 135 | { 136 | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) { 137 | $this->throwError($tokenType); 138 | } 139 | 140 | if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) { 141 | if ($this->newline === null) { 142 | $this->detectNewline(); 143 | } 144 | } 145 | 146 | $this->next(); 147 | } 148 | 149 | /** 150 | * @throws ParserException 151 | */ 152 | public function consumeTokenValue(int $tokenType, string $tokenValue): void 153 | { 154 | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType || $this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) { 155 | $this->throwError($tokenType, $tokenValue); 156 | } 157 | 158 | $this->next(); 159 | } 160 | 161 | /** @phpstan-impure */ 162 | public function tryConsumeTokenValue(string $tokenValue): bool 163 | { 164 | if ($this->tokens[$this->index][Lexer::VALUE_OFFSET] !== $tokenValue) { 165 | return false; 166 | } 167 | 168 | $this->next(); 169 | 170 | return true; 171 | } 172 | 173 | /** 174 | * @return list 175 | */ 176 | public function flushComments(): array 177 | { 178 | $res = $this->comments; 179 | $this->comments = []; 180 | return $res; 181 | } 182 | 183 | /** @phpstan-impure */ 184 | public function tryConsumeTokenType(int $tokenType): bool 185 | { 186 | if ($this->tokens[$this->index][Lexer::TYPE_OFFSET] !== $tokenType) { 187 | return false; 188 | } 189 | 190 | if ($tokenType === Lexer::TOKEN_PHPDOC_EOL) { 191 | if ($this->newline === null) { 192 | $this->detectNewline(); 193 | } 194 | } 195 | 196 | $this->next(); 197 | 198 | return true; 199 | } 200 | 201 | /** 202 | * @deprecated Use skipNewLineTokensAndConsumeComments instead (when parsing a type) 203 | */ 204 | public function skipNewLineTokens(): void 205 | { 206 | if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { 207 | return; 208 | } 209 | 210 | do { 211 | $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); 212 | } while ($foundNewLine === true); 213 | } 214 | 215 | public function skipNewLineTokensAndConsumeComments(): void 216 | { 217 | if ($this->currentTokenType() === Lexer::TOKEN_COMMENT) { 218 | $this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex()); 219 | $this->next(); 220 | } 221 | 222 | if (!$this->isCurrentTokenType(Lexer::TOKEN_PHPDOC_EOL)) { 223 | return; 224 | } 225 | 226 | do { 227 | $foundNewLine = $this->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); 228 | if ($this->currentTokenType() !== Lexer::TOKEN_COMMENT) { 229 | continue; 230 | } 231 | 232 | $this->comments[] = new Comment($this->currentTokenValue(), $this->currentTokenLine(), $this->currentTokenIndex()); 233 | $this->next(); 234 | } while ($foundNewLine === true); 235 | } 236 | 237 | private function detectNewline(): void 238 | { 239 | $value = $this->currentTokenValue(); 240 | if (substr($value, 0, 2) === "\r\n") { 241 | $this->newline = "\r\n"; 242 | } elseif (substr($value, 0, 1) === "\n") { 243 | $this->newline = "\n"; 244 | } 245 | } 246 | 247 | public function getSkippedHorizontalWhiteSpaceIfAny(): string 248 | { 249 | if ($this->index > 0 && $this->tokens[$this->index - 1][Lexer::TYPE_OFFSET] === Lexer::TOKEN_HORIZONTAL_WS) { 250 | return $this->tokens[$this->index - 1][Lexer::VALUE_OFFSET]; 251 | } 252 | 253 | return ''; 254 | } 255 | 256 | /** @phpstan-impure */ 257 | public function joinUntil(int ...$tokenType): string 258 | { 259 | $s = ''; 260 | while (!in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $tokenType, true)) { 261 | $s .= $this->tokens[$this->index++][Lexer::VALUE_OFFSET]; 262 | } 263 | return $s; 264 | } 265 | 266 | public function next(): void 267 | { 268 | $this->index++; 269 | $this->skipIrrelevantTokens(); 270 | } 271 | 272 | private function skipIrrelevantTokens(): void 273 | { 274 | if (!isset($this->tokens[$this->index])) { 275 | return; 276 | } 277 | 278 | while (in_array($this->tokens[$this->index][Lexer::TYPE_OFFSET], $this->skippedTokenTypes, true)) { 279 | if (!isset($this->tokens[$this->index + 1])) { 280 | break; 281 | } 282 | $this->index++; 283 | } 284 | } 285 | 286 | public function addEndOfLineToSkippedTokens(): void 287 | { 288 | $this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS, Lexer::TOKEN_PHPDOC_EOL]; 289 | } 290 | 291 | public function removeEndOfLineFromSkippedTokens(): void 292 | { 293 | $this->skippedTokenTypes = [Lexer::TOKEN_HORIZONTAL_WS]; 294 | } 295 | 296 | /** @phpstan-impure */ 297 | public function forwardToTheEnd(): void 298 | { 299 | $lastToken = count($this->tokens) - 1; 300 | $this->index = $lastToken; 301 | } 302 | 303 | public function pushSavePoint(): void 304 | { 305 | $this->savePoints[] = [$this->index, $this->comments]; 306 | } 307 | 308 | public function dropSavePoint(): void 309 | { 310 | array_pop($this->savePoints); 311 | } 312 | 313 | public function rollback(): void 314 | { 315 | $savepoint = array_pop($this->savePoints); 316 | assert($savepoint !== null); 317 | [$this->index, $this->comments] = $savepoint; 318 | } 319 | 320 | /** 321 | * @throws ParserException 322 | */ 323 | private function throwError(int $expectedTokenType, ?string $expectedTokenValue = null): void 324 | { 325 | throw new ParserException( 326 | $this->currentTokenValue(), 327 | $this->currentTokenType(), 328 | $this->currentTokenOffset(), 329 | $expectedTokenType, 330 | $expectedTokenValue, 331 | $this->currentTokenLine(), 332 | ); 333 | } 334 | 335 | /** 336 | * Check whether the position is directly preceded by a certain token type. 337 | * 338 | * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped 339 | */ 340 | public function hasTokenImmediatelyBefore(int $pos, int $expectedTokenType): bool 341 | { 342 | $tokens = $this->tokens; 343 | $pos--; 344 | for (; $pos >= 0; $pos--) { 345 | $token = $tokens[$pos]; 346 | $type = $token[Lexer::TYPE_OFFSET]; 347 | if ($type === $expectedTokenType) { 348 | return true; 349 | } 350 | if (!in_array($type, [ 351 | Lexer::TOKEN_HORIZONTAL_WS, 352 | Lexer::TOKEN_PHPDOC_EOL, 353 | ], true)) { 354 | break; 355 | } 356 | } 357 | return false; 358 | } 359 | 360 | /** 361 | * Check whether the position is directly followed by a certain token type. 362 | * 363 | * During this check TOKEN_HORIZONTAL_WS and TOKEN_PHPDOC_EOL are skipped 364 | */ 365 | public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool 366 | { 367 | $tokens = $this->tokens; 368 | $pos++; 369 | for ($c = count($tokens); $pos < $c; $pos++) { 370 | $token = $tokens[$pos]; 371 | $type = $token[Lexer::TYPE_OFFSET]; 372 | if ($type === $expectedTokenType) { 373 | return true; 374 | } 375 | if (!in_array($type, [ 376 | Lexer::TOKEN_HORIZONTAL_WS, 377 | Lexer::TOKEN_PHPDOC_EOL, 378 | ], true)) { 379 | break; 380 | } 381 | } 382 | 383 | return false; 384 | } 385 | 386 | public function getDetectedNewline(): ?string 387 | { 388 | return $this->newline; 389 | } 390 | 391 | /** 392 | * Whether the given position is immediately surrounded by parenthesis. 393 | */ 394 | public function hasParentheses(int $startPos, int $endPos): bool 395 | { 396 | return $this->hasTokenImmediatelyBefore($startPos, Lexer::TOKEN_OPEN_PARENTHESES) 397 | && $this->hasTokenImmediatelyAfter($endPos, Lexer::TOKEN_CLOSE_PARENTHESES); 398 | } 399 | 400 | } 401 | -------------------------------------------------------------------------------- /src/Ast/PhpDoc/PhpDocNode.php: -------------------------------------------------------------------------------- 1 | children = $children; 26 | } 27 | 28 | /** 29 | * @return PhpDocTagNode[] 30 | */ 31 | public function getTags(): array 32 | { 33 | return array_filter($this->children, static fn (PhpDocChildNode $child): bool => $child instanceof PhpDocTagNode); 34 | } 35 | 36 | /** 37 | * @return PhpDocTagNode[] 38 | */ 39 | public function getTagsByName(string $tagName): array 40 | { 41 | return array_filter($this->getTags(), static fn (PhpDocTagNode $tag): bool => $tag->name === $tagName); 42 | } 43 | 44 | /** 45 | * @return VarTagValueNode[] 46 | */ 47 | public function getVarTagValues(string $tagName = '@var'): array 48 | { 49 | return array_filter( 50 | array_column($this->getTagsByName($tagName), 'value'), 51 | static fn (PhpDocTagValueNode $value): bool => $value instanceof VarTagValueNode, 52 | ); 53 | } 54 | 55 | /** 56 | * @return ParamTagValueNode[] 57 | */ 58 | public function getParamTagValues(string $tagName = '@param'): array 59 | { 60 | return array_filter( 61 | array_column($this->getTagsByName($tagName), 'value'), 62 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamTagValueNode, 63 | ); 64 | } 65 | 66 | /** 67 | * @return TypelessParamTagValueNode[] 68 | */ 69 | public function getTypelessParamTagValues(string $tagName = '@param'): array 70 | { 71 | return array_filter( 72 | array_column($this->getTagsByName($tagName), 'value'), 73 | static fn (PhpDocTagValueNode $value): bool => $value instanceof TypelessParamTagValueNode, 74 | ); 75 | } 76 | 77 | /** 78 | * @return ParamImmediatelyInvokedCallableTagValueNode[] 79 | */ 80 | public function getParamImmediatelyInvokedCallableTagValues(string $tagName = '@param-immediately-invoked-callable'): array 81 | { 82 | return array_filter( 83 | array_column($this->getTagsByName($tagName), 'value'), 84 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamImmediatelyInvokedCallableTagValueNode, 85 | ); 86 | } 87 | 88 | /** 89 | * @return ParamLaterInvokedCallableTagValueNode[] 90 | */ 91 | public function getParamLaterInvokedCallableTagValues(string $tagName = '@param-later-invoked-callable'): array 92 | { 93 | return array_filter( 94 | array_column($this->getTagsByName($tagName), 'value'), 95 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamLaterInvokedCallableTagValueNode, 96 | ); 97 | } 98 | 99 | /** 100 | * @return ParamClosureThisTagValueNode[] 101 | */ 102 | public function getParamClosureThisTagValues(string $tagName = '@param-closure-this'): array 103 | { 104 | return array_filter( 105 | array_column($this->getTagsByName($tagName), 'value'), 106 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamClosureThisTagValueNode, 107 | ); 108 | } 109 | 110 | /** 111 | * @return PureUnlessCallableIsImpureTagValueNode[] 112 | */ 113 | public function getPureUnlessCallableIsImpureTagValues(string $tagName = '@pure-unless-callable-is-impure'): array 114 | { 115 | return array_filter( 116 | array_column($this->getTagsByName($tagName), 'value'), 117 | static fn (PhpDocTagValueNode $value): bool => $value instanceof PureUnlessCallableIsImpureTagValueNode, 118 | ); 119 | } 120 | 121 | /** 122 | * @return TemplateTagValueNode[] 123 | */ 124 | public function getTemplateTagValues(string $tagName = '@template'): array 125 | { 126 | return array_filter( 127 | array_column($this->getTagsByName($tagName), 'value'), 128 | static fn (PhpDocTagValueNode $value): bool => $value instanceof TemplateTagValueNode, 129 | ); 130 | } 131 | 132 | /** 133 | * @return ExtendsTagValueNode[] 134 | */ 135 | public function getExtendsTagValues(string $tagName = '@extends'): array 136 | { 137 | return array_filter( 138 | array_column($this->getTagsByName($tagName), 'value'), 139 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ExtendsTagValueNode, 140 | ); 141 | } 142 | 143 | /** 144 | * @return ImplementsTagValueNode[] 145 | */ 146 | public function getImplementsTagValues(string $tagName = '@implements'): array 147 | { 148 | return array_filter( 149 | array_column($this->getTagsByName($tagName), 'value'), 150 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ImplementsTagValueNode, 151 | ); 152 | } 153 | 154 | /** 155 | * @return UsesTagValueNode[] 156 | */ 157 | public function getUsesTagValues(string $tagName = '@use'): array 158 | { 159 | return array_filter( 160 | array_column($this->getTagsByName($tagName), 'value'), 161 | static fn (PhpDocTagValueNode $value): bool => $value instanceof UsesTagValueNode, 162 | ); 163 | } 164 | 165 | /** 166 | * @return ReturnTagValueNode[] 167 | */ 168 | public function getReturnTagValues(string $tagName = '@return'): array 169 | { 170 | return array_filter( 171 | array_column($this->getTagsByName($tagName), 'value'), 172 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ReturnTagValueNode, 173 | ); 174 | } 175 | 176 | /** 177 | * @return ThrowsTagValueNode[] 178 | */ 179 | public function getThrowsTagValues(string $tagName = '@throws'): array 180 | { 181 | return array_filter( 182 | array_column($this->getTagsByName($tagName), 'value'), 183 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ThrowsTagValueNode, 184 | ); 185 | } 186 | 187 | /** 188 | * @return MixinTagValueNode[] 189 | */ 190 | public function getMixinTagValues(string $tagName = '@mixin'): array 191 | { 192 | return array_filter( 193 | array_column($this->getTagsByName($tagName), 'value'), 194 | static fn (PhpDocTagValueNode $value): bool => $value instanceof MixinTagValueNode, 195 | ); 196 | } 197 | 198 | /** 199 | * @return RequireExtendsTagValueNode[] 200 | */ 201 | public function getRequireExtendsTagValues(string $tagName = '@phpstan-require-extends'): array 202 | { 203 | return array_filter( 204 | array_column($this->getTagsByName($tagName), 'value'), 205 | static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireExtendsTagValueNode, 206 | ); 207 | } 208 | 209 | /** 210 | * @return RequireImplementsTagValueNode[] 211 | */ 212 | public function getRequireImplementsTagValues(string $tagName = '@phpstan-require-implements'): array 213 | { 214 | return array_filter( 215 | array_column($this->getTagsByName($tagName), 'value'), 216 | static fn (PhpDocTagValueNode $value): bool => $value instanceof RequireImplementsTagValueNode, 217 | ); 218 | } 219 | 220 | /** 221 | * @return SealedTagValueNode[] 222 | */ 223 | public function getSealedTagValues(string $tagName = '@phpstan-sealed'): array 224 | { 225 | return array_filter( 226 | array_column($this->getTagsByName($tagName), 'value'), 227 | static fn (PhpDocTagValueNode $value): bool => $value instanceof SealedTagValueNode, 228 | ); 229 | } 230 | 231 | /** 232 | * @return DeprecatedTagValueNode[] 233 | */ 234 | public function getDeprecatedTagValues(): array 235 | { 236 | return array_filter( 237 | array_column($this->getTagsByName('@deprecated'), 'value'), 238 | static fn (PhpDocTagValueNode $value): bool => $value instanceof DeprecatedTagValueNode, 239 | ); 240 | } 241 | 242 | /** 243 | * @return PropertyTagValueNode[] 244 | */ 245 | public function getPropertyTagValues(string $tagName = '@property'): array 246 | { 247 | return array_filter( 248 | array_column($this->getTagsByName($tagName), 'value'), 249 | static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode, 250 | ); 251 | } 252 | 253 | /** 254 | * @return PropertyTagValueNode[] 255 | */ 256 | public function getPropertyReadTagValues(string $tagName = '@property-read'): array 257 | { 258 | return array_filter( 259 | array_column($this->getTagsByName($tagName), 'value'), 260 | static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode, 261 | ); 262 | } 263 | 264 | /** 265 | * @return PropertyTagValueNode[] 266 | */ 267 | public function getPropertyWriteTagValues(string $tagName = '@property-write'): array 268 | { 269 | return array_filter( 270 | array_column($this->getTagsByName($tagName), 'value'), 271 | static fn (PhpDocTagValueNode $value): bool => $value instanceof PropertyTagValueNode, 272 | ); 273 | } 274 | 275 | /** 276 | * @return MethodTagValueNode[] 277 | */ 278 | public function getMethodTagValues(string $tagName = '@method'): array 279 | { 280 | return array_filter( 281 | array_column($this->getTagsByName($tagName), 'value'), 282 | static fn (PhpDocTagValueNode $value): bool => $value instanceof MethodTagValueNode, 283 | ); 284 | } 285 | 286 | /** 287 | * @return TypeAliasTagValueNode[] 288 | */ 289 | public function getTypeAliasTagValues(string $tagName = '@phpstan-type'): array 290 | { 291 | return array_filter( 292 | array_column($this->getTagsByName($tagName), 'value'), 293 | static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasTagValueNode, 294 | ); 295 | } 296 | 297 | /** 298 | * @return TypeAliasImportTagValueNode[] 299 | */ 300 | public function getTypeAliasImportTagValues(string $tagName = '@phpstan-import-type'): array 301 | { 302 | return array_filter( 303 | array_column($this->getTagsByName($tagName), 'value'), 304 | static fn (PhpDocTagValueNode $value): bool => $value instanceof TypeAliasImportTagValueNode, 305 | ); 306 | } 307 | 308 | /** 309 | * @return AssertTagValueNode[] 310 | */ 311 | public function getAssertTagValues(string $tagName = '@phpstan-assert'): array 312 | { 313 | return array_filter( 314 | array_column($this->getTagsByName($tagName), 'value'), 315 | static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagValueNode, 316 | ); 317 | } 318 | 319 | /** 320 | * @return AssertTagPropertyValueNode[] 321 | */ 322 | public function getAssertPropertyTagValues(string $tagName = '@phpstan-assert'): array 323 | { 324 | return array_filter( 325 | array_column($this->getTagsByName($tagName), 'value'), 326 | static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagPropertyValueNode, 327 | ); 328 | } 329 | 330 | /** 331 | * @return AssertTagMethodValueNode[] 332 | */ 333 | public function getAssertMethodTagValues(string $tagName = '@phpstan-assert'): array 334 | { 335 | return array_filter( 336 | array_column($this->getTagsByName($tagName), 'value'), 337 | static fn (PhpDocTagValueNode $value): bool => $value instanceof AssertTagMethodValueNode, 338 | ); 339 | } 340 | 341 | /** 342 | * @return SelfOutTagValueNode[] 343 | */ 344 | public function getSelfOutTypeTagValues(string $tagName = '@phpstan-this-out'): array 345 | { 346 | return array_filter( 347 | array_column($this->getTagsByName($tagName), 'value'), 348 | static fn (PhpDocTagValueNode $value): bool => $value instanceof SelfOutTagValueNode, 349 | ); 350 | } 351 | 352 | /** 353 | * @return ParamOutTagValueNode[] 354 | */ 355 | public function getParamOutTypeTagValues(string $tagName = '@param-out'): array 356 | { 357 | return array_filter( 358 | array_column($this->getTagsByName($tagName), 'value'), 359 | static fn (PhpDocTagValueNode $value): bool => $value instanceof ParamOutTagValueNode, 360 | ); 361 | } 362 | 363 | public function __toString(): string 364 | { 365 | $children = array_map( 366 | static function (PhpDocChildNode $child): string { 367 | $s = (string) $child; 368 | return $s === '' ? '' : ' ' . $s; 369 | }, 370 | $this->children, 371 | ); 372 | return "/**\n *" . implode("\n *", $children) . "\n */"; 373 | } 374 | 375 | } 376 | -------------------------------------------------------------------------------- /src/Printer/Printer.php: -------------------------------------------------------------------------------- 1 | */ 96 | private Differ $differ; 97 | 98 | /** 99 | * Map From "{$class}->{$subNode}" to string that should be inserted 100 | * between elements of this list subnode 101 | * 102 | * @var array 103 | */ 104 | private array $listInsertionMap = [ 105 | PhpDocNode::class . '->children' => "\n * ", 106 | UnionTypeNode::class . '->types' => '|', 107 | IntersectionTypeNode::class . '->types' => '&', 108 | ArrayShapeNode::class . '->items' => ', ', 109 | ObjectShapeNode::class . '->items' => ', ', 110 | CallableTypeNode::class . '->parameters' => ', ', 111 | CallableTypeNode::class . '->templateTypes' => ', ', 112 | GenericTypeNode::class . '->genericTypes' => ', ', 113 | ConstExprArrayNode::class . '->items' => ', ', 114 | MethodTagValueNode::class . '->parameters' => ', ', 115 | DoctrineArray::class . '->items' => ', ', 116 | DoctrineAnnotation::class . '->arguments' => ', ', 117 | ]; 118 | 119 | /** 120 | * [$find, $extraLeft, $extraRight] 121 | * 122 | * @var array 123 | */ 124 | private array $emptyListInsertionMap = [ 125 | CallableTypeNode::class . '->parameters' => ['(', '', ''], 126 | ArrayShapeNode::class . '->items' => ['{', '', ''], 127 | ObjectShapeNode::class . '->items' => ['{', '', ''], 128 | DoctrineArray::class . '->items' => ['{', '', ''], 129 | DoctrineAnnotation::class . '->arguments' => ['(', '', ''], 130 | ]; 131 | 132 | /** @var array>> */ 133 | private array $parenthesesMap = [ 134 | CallableTypeNode::class . '->returnType' => [ 135 | CallableTypeNode::class, 136 | UnionTypeNode::class, 137 | IntersectionTypeNode::class, 138 | ], 139 | ArrayTypeNode::class . '->type' => [ 140 | CallableTypeNode::class, 141 | UnionTypeNode::class, 142 | IntersectionTypeNode::class, 143 | ConstTypeNode::class, 144 | NullableTypeNode::class, 145 | ], 146 | OffsetAccessTypeNode::class . '->type' => [ 147 | CallableTypeNode::class, 148 | UnionTypeNode::class, 149 | IntersectionTypeNode::class, 150 | NullableTypeNode::class, 151 | ], 152 | ]; 153 | 154 | /** @var array>> */ 155 | private array $parenthesesListMap = [ 156 | IntersectionTypeNode::class . '->types' => [ 157 | IntersectionTypeNode::class, 158 | UnionTypeNode::class, 159 | NullableTypeNode::class, 160 | ], 161 | UnionTypeNode::class . '->types' => [ 162 | IntersectionTypeNode::class, 163 | UnionTypeNode::class, 164 | NullableTypeNode::class, 165 | ], 166 | ]; 167 | 168 | public function printFormatPreserving(PhpDocNode $node, PhpDocNode $originalNode, TokenIterator $originalTokens): string 169 | { 170 | $this->differ = new Differ(static function ($a, $b) { 171 | if ($a instanceof Node && $b instanceof Node) { 172 | return $a === $b->getAttribute(Attribute::ORIGINAL_NODE); 173 | } 174 | 175 | return false; 176 | }); 177 | 178 | $tokenIndex = 0; 179 | $result = $this->printArrayFormatPreserving( 180 | $node->children, 181 | $originalNode->children, 182 | $originalTokens, 183 | $tokenIndex, 184 | PhpDocNode::class, 185 | 'children', 186 | ); 187 | if ($result !== null) { 188 | return $result . $originalTokens->getContentBetween($tokenIndex, $originalTokens->getTokenCount()); 189 | } 190 | 191 | return $this->print($node); 192 | } 193 | 194 | public function print(Node $node): string 195 | { 196 | if ($node instanceof PhpDocNode) { 197 | return "/**\n *" . implode("\n *", array_map( 198 | function (PhpDocChildNode $child): string { 199 | $s = $this->print($child); 200 | return $s === '' ? '' : ' ' . $s; 201 | }, 202 | $node->children, 203 | )) . "\n */"; 204 | } 205 | if ($node instanceof PhpDocTextNode) { 206 | return $node->text; 207 | } 208 | if ($node instanceof PhpDocTagNode) { 209 | if ($node->value instanceof DoctrineTagValueNode) { 210 | return $this->print($node->value); 211 | } 212 | 213 | return trim(sprintf('%s %s', $node->name, $this->print($node->value))); 214 | } 215 | if ($node instanceof PhpDocTagValueNode) { 216 | return $this->printTagValue($node); 217 | } 218 | if ($node instanceof TypeNode) { 219 | return $this->printType($node); 220 | } 221 | if ($node instanceof ConstExprNode) { 222 | return $this->printConstExpr($node); 223 | } 224 | if ($node instanceof MethodTagValueParameterNode) { 225 | $type = $node->type !== null ? $this->print($node->type) . ' ' : ''; 226 | $isReference = $node->isReference ? '&' : ''; 227 | $isVariadic = $node->isVariadic ? '...' : ''; 228 | $default = $node->defaultValue !== null ? ' = ' . $this->print($node->defaultValue) : ''; 229 | return "{$type}{$isReference}{$isVariadic}{$node->parameterName}{$default}"; 230 | } 231 | if ($node instanceof CallableTypeParameterNode) { 232 | $type = $this->print($node->type) . ' '; 233 | $isReference = $node->isReference ? '&' : ''; 234 | $isVariadic = $node->isVariadic ? '...' : ''; 235 | $isOptional = $node->isOptional ? '=' : ''; 236 | return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional; 237 | } 238 | if ($node instanceof ArrayShapeUnsealedTypeNode) { 239 | if ($node->keyType !== null) { 240 | return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType)); 241 | } 242 | return sprintf('<%s>', $this->printType($node->valueType)); 243 | } 244 | if ($node instanceof DoctrineAnnotation) { 245 | return (string) $node; 246 | } 247 | if ($node instanceof DoctrineArgument) { 248 | return (string) $node; 249 | } 250 | if ($node instanceof DoctrineArray) { 251 | return (string) $node; 252 | } 253 | if ($node instanceof DoctrineArrayItem) { 254 | return (string) $node; 255 | } 256 | if ($node instanceof ArrayShapeItemNode) { 257 | if ($node->keyName !== null) { 258 | return sprintf( 259 | '%s%s: %s', 260 | $this->print($node->keyName), 261 | $node->optional ? '?' : '', 262 | $this->printType($node->valueType), 263 | ); 264 | } 265 | 266 | return $this->printType($node->valueType); 267 | } 268 | if ($node instanceof ObjectShapeItemNode) { 269 | if ($node->keyName !== null) { 270 | return sprintf( 271 | '%s%s: %s', 272 | $this->print($node->keyName), 273 | $node->optional ? '?' : '', 274 | $this->printType($node->valueType), 275 | ); 276 | } 277 | 278 | return $this->printType($node->valueType); 279 | } 280 | 281 | throw new LogicException(sprintf('Unknown node type %s', get_class($node))); 282 | } 283 | 284 | private function printTagValue(PhpDocTagValueNode $node): string 285 | { 286 | // only nodes that contain another node are handled here 287 | // the rest falls back on (string) $node 288 | 289 | if ($node instanceof AssertTagMethodValueNode) { 290 | $isNegated = $node->isNegated ? '!' : ''; 291 | $isEquality = $node->isEquality ? '=' : ''; 292 | $type = $this->printType($node->type); 293 | return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->method}() {$node->description}"); 294 | } 295 | if ($node instanceof AssertTagPropertyValueNode) { 296 | $isNegated = $node->isNegated ? '!' : ''; 297 | $isEquality = $node->isEquality ? '=' : ''; 298 | $type = $this->printType($node->type); 299 | return trim("{$isNegated}{$isEquality}{$type} {$node->parameter}->{$node->property} {$node->description}"); 300 | } 301 | if ($node instanceof AssertTagValueNode) { 302 | $isNegated = $node->isNegated ? '!' : ''; 303 | $isEquality = $node->isEquality ? '=' : ''; 304 | $type = $this->printType($node->type); 305 | return trim("{$isNegated}{$isEquality}{$type} {$node->parameter} {$node->description}"); 306 | } 307 | if ($node instanceof ExtendsTagValueNode || $node instanceof ImplementsTagValueNode) { 308 | $type = $this->printType($node->type); 309 | return trim("{$type} {$node->description}"); 310 | } 311 | if ($node instanceof MethodTagValueNode) { 312 | $static = $node->isStatic ? 'static ' : ''; 313 | $returnType = $node->returnType !== null ? $this->printType($node->returnType) . ' ' : ''; 314 | $parameters = implode(', ', array_map(fn (MethodTagValueParameterNode $parameter): string => $this->print($parameter), $node->parameters)); 315 | $description = $node->description !== '' ? " {$node->description}" : ''; 316 | $templateTypes = count($node->templateTypes) > 0 ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateTag): string => $this->print($templateTag), $node->templateTypes)) . '>' : ''; 317 | return "{$static}{$returnType}{$node->methodName}{$templateTypes}({$parameters}){$description}"; 318 | } 319 | if ($node instanceof MixinTagValueNode) { 320 | $type = $this->printType($node->type); 321 | return trim("{$type} {$node->description}"); 322 | } 323 | if ($node instanceof RequireExtendsTagValueNode) { 324 | $type = $this->printType($node->type); 325 | return trim("{$type} {$node->description}"); 326 | } 327 | if ($node instanceof RequireImplementsTagValueNode) { 328 | $type = $this->printType($node->type); 329 | return trim("{$type} {$node->description}"); 330 | } 331 | if ($node instanceof SealedTagValueNode) { 332 | $type = $this->printType($node->type); 333 | return trim("{$type} {$node->description}"); 334 | } 335 | if ($node instanceof ParamOutTagValueNode) { 336 | $type = $this->printType($node->type); 337 | return trim("{$type} {$node->parameterName} {$node->description}"); 338 | } 339 | if ($node instanceof ParamTagValueNode) { 340 | $reference = $node->isReference ? '&' : ''; 341 | $variadic = $node->isVariadic ? '...' : ''; 342 | $type = $this->printType($node->type); 343 | return trim("{$type} {$reference}{$variadic}{$node->parameterName} {$node->description}"); 344 | } 345 | if ($node instanceof ParamImmediatelyInvokedCallableTagValueNode) { 346 | return trim("{$node->parameterName} {$node->description}"); 347 | } 348 | if ($node instanceof ParamLaterInvokedCallableTagValueNode) { 349 | return trim("{$node->parameterName} {$node->description}"); 350 | } 351 | if ($node instanceof ParamClosureThisTagValueNode) { 352 | return trim("{$node->type} {$node->parameterName} {$node->description}"); 353 | } 354 | if ($node instanceof PureUnlessCallableIsImpureTagValueNode) { 355 | return trim("{$node->parameterName} {$node->description}"); 356 | } 357 | if ($node instanceof PropertyTagValueNode) { 358 | $type = $this->printType($node->type); 359 | return trim("{$type} {$node->propertyName} {$node->description}"); 360 | } 361 | if ($node instanceof ReturnTagValueNode) { 362 | $type = $this->printType($node->type); 363 | return trim("{$type} {$node->description}"); 364 | } 365 | if ($node instanceof SelfOutTagValueNode) { 366 | $type = $this->printType($node->type); 367 | return trim($type . ' ' . $node->description); 368 | } 369 | if ($node instanceof TemplateTagValueNode) { 370 | $upperBound = $node->bound !== null ? ' of ' . $this->printType($node->bound) : ''; 371 | $lowerBound = $node->lowerBound !== null ? ' super ' . $this->printType($node->lowerBound) : ''; 372 | $default = $node->default !== null ? ' = ' . $this->printType($node->default) : ''; 373 | return trim("{$node->name}{$upperBound}{$lowerBound}{$default} {$node->description}"); 374 | } 375 | if ($node instanceof ThrowsTagValueNode) { 376 | $type = $this->printType($node->type); 377 | return trim("{$type} {$node->description}"); 378 | } 379 | if ($node instanceof TypeAliasImportTagValueNode) { 380 | return trim( 381 | "{$node->importedAlias} from " . $this->printType($node->importedFrom) 382 | . ($node->importedAs !== null ? " as {$node->importedAs}" : ''), 383 | ); 384 | } 385 | if ($node instanceof TypeAliasTagValueNode) { 386 | $type = $this->printType($node->type); 387 | return trim("{$node->alias} {$type}"); 388 | } 389 | if ($node instanceof UsesTagValueNode) { 390 | $type = $this->printType($node->type); 391 | return trim("{$type} {$node->description}"); 392 | } 393 | if ($node instanceof VarTagValueNode) { 394 | $type = $this->printType($node->type); 395 | return trim("{$type} " . trim("{$node->variableName} {$node->description}")); 396 | } 397 | 398 | return (string) $node; 399 | } 400 | 401 | private function printType(TypeNode $node): string 402 | { 403 | if ($node instanceof ArrayShapeNode) { 404 | $items = array_map(fn (ArrayShapeItemNode $item): string => $this->print($item), $node->items); 405 | 406 | if (! $node->sealed) { 407 | $items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType)); 408 | } 409 | 410 | return $node->kind . '{' . implode(', ', $items) . '}'; 411 | } 412 | if ($node instanceof ArrayTypeNode) { 413 | return $this->printOffsetAccessType($node->type) . '[]'; 414 | } 415 | if ($node instanceof CallableTypeNode) { 416 | if ($node->returnType instanceof CallableTypeNode || $node->returnType instanceof UnionTypeNode || $node->returnType instanceof IntersectionTypeNode) { 417 | $returnType = $this->wrapInParentheses($node->returnType); 418 | } else { 419 | $returnType = $this->printType($node->returnType); 420 | } 421 | $template = $node->templateTypes !== [] 422 | ? '<' . implode(', ', array_map(fn (TemplateTagValueNode $templateNode): string => $this->print($templateNode), $node->templateTypes)) . '>' 423 | : ''; 424 | $parameters = implode(', ', array_map(fn (CallableTypeParameterNode $parameterNode): string => $this->print($parameterNode), $node->parameters)); 425 | return "{$node->identifier}{$template}({$parameters}): {$returnType}"; 426 | } 427 | if ($node instanceof ConditionalTypeForParameterNode) { 428 | return sprintf( 429 | '(%s %s %s ? %s : %s)', 430 | $node->parameterName, 431 | $node->negated ? 'is not' : 'is', 432 | $this->printType($node->targetType), 433 | $this->printType($node->if), 434 | $this->printType($node->else), 435 | ); 436 | } 437 | if ($node instanceof ConditionalTypeNode) { 438 | return sprintf( 439 | '(%s %s %s ? %s : %s)', 440 | $this->printType($node->subjectType), 441 | $node->negated ? 'is not' : 'is', 442 | $this->printType($node->targetType), 443 | $this->printType($node->if), 444 | $this->printType($node->else), 445 | ); 446 | } 447 | if ($node instanceof ConstTypeNode) { 448 | return $this->printConstExpr($node->constExpr); 449 | } 450 | if ($node instanceof GenericTypeNode) { 451 | $genericTypes = []; 452 | 453 | foreach ($node->genericTypes as $index => $type) { 454 | $variance = $node->variances[$index] ?? GenericTypeNode::VARIANCE_INVARIANT; 455 | if ($variance === GenericTypeNode::VARIANCE_INVARIANT) { 456 | $genericTypes[] = $this->printType($type); 457 | } elseif ($variance === GenericTypeNode::VARIANCE_BIVARIANT) { 458 | $genericTypes[] = '*'; 459 | } else { 460 | $genericTypes[] = sprintf('%s %s', $variance, $this->print($type)); 461 | } 462 | } 463 | 464 | return $node->type . '<' . implode(', ', $genericTypes) . '>'; 465 | } 466 | if ($node instanceof IdentifierTypeNode) { 467 | return $node->name; 468 | } 469 | if ($node instanceof IntersectionTypeNode || $node instanceof UnionTypeNode) { 470 | $items = []; 471 | foreach ($node->types as $type) { 472 | if ( 473 | $type instanceof IntersectionTypeNode 474 | || $type instanceof UnionTypeNode 475 | || $type instanceof NullableTypeNode 476 | ) { 477 | $items[] = $this->wrapInParentheses($type); 478 | continue; 479 | } 480 | 481 | $items[] = $this->printType($type); 482 | } 483 | 484 | return implode($node instanceof IntersectionTypeNode ? '&' : '|', $items); 485 | } 486 | if ($node instanceof InvalidTypeNode) { 487 | return (string) $node; 488 | } 489 | if ($node instanceof NullableTypeNode) { 490 | if ($node->type instanceof IntersectionTypeNode || $node->type instanceof UnionTypeNode) { 491 | return '?(' . $this->printType($node->type) . ')'; 492 | } 493 | 494 | return '?' . $this->printType($node->type); 495 | } 496 | if ($node instanceof ObjectShapeNode) { 497 | $items = array_map(fn (ObjectShapeItemNode $item): string => $this->print($item), $node->items); 498 | 499 | return 'object{' . implode(', ', $items) . '}'; 500 | } 501 | if ($node instanceof OffsetAccessTypeNode) { 502 | return $this->printOffsetAccessType($node->type) . '[' . $this->printType($node->offset) . ']'; 503 | } 504 | if ($node instanceof ThisTypeNode) { 505 | return (string) $node; 506 | } 507 | 508 | throw new LogicException(sprintf('Unknown node type %s', get_class($node))); 509 | } 510 | 511 | private function wrapInParentheses(TypeNode $node): string 512 | { 513 | return '(' . $this->printType($node) . ')'; 514 | } 515 | 516 | private function printOffsetAccessType(TypeNode $type): string 517 | { 518 | if ( 519 | $type instanceof CallableTypeNode 520 | || $type instanceof UnionTypeNode 521 | || $type instanceof IntersectionTypeNode 522 | || $type instanceof NullableTypeNode 523 | ) { 524 | return $this->wrapInParentheses($type); 525 | } 526 | 527 | return $this->printType($type); 528 | } 529 | 530 | private function printConstExpr(ConstExprNode $node): string 531 | { 532 | // this is fine - ConstExprNode classes do not contain nodes that need smart printer logic 533 | return (string) $node; 534 | } 535 | 536 | /** 537 | * @param Node[] $nodes 538 | * @param Node[] $originalNodes 539 | */ 540 | private function printArrayFormatPreserving(array $nodes, array $originalNodes, TokenIterator $originalTokens, int &$tokenIndex, string $parentNodeClass, string $subNodeName): ?string 541 | { 542 | $diff = $this->differ->diffWithReplacements($originalNodes, $nodes); 543 | $mapKey = $parentNodeClass . '->' . $subNodeName; 544 | $insertStr = $this->listInsertionMap[$mapKey] ?? null; 545 | $result = ''; 546 | $beforeFirstKeepOrReplace = true; 547 | $delayedAdd = []; 548 | 549 | $insertNewline = false; 550 | [$isMultiline, $beforeAsteriskIndent, $afterAsteriskIndent] = $this->isMultiline($tokenIndex, $originalNodes, $originalTokens); 551 | 552 | if ($insertStr === "\n * ") { 553 | $insertStr = sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 554 | } 555 | 556 | foreach ($diff as $i => $diffElem) { 557 | $diffType = $diffElem->type; 558 | $arrItem = $diffElem->new; 559 | $origArrayItem = $diffElem->old; 560 | if ($diffType === DiffElem::TYPE_KEEP || $diffType === DiffElem::TYPE_REPLACE) { 561 | $beforeFirstKeepOrReplace = false; 562 | if (!$arrItem instanceof Node || !$origArrayItem instanceof Node) { 563 | return null; 564 | } 565 | 566 | /** @var int $itemStartPos */ 567 | $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX); 568 | 569 | /** @var int $itemEndPos */ 570 | $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX); 571 | 572 | if ($itemStartPos < 0 || $itemEndPos < 0 || $itemStartPos < $tokenIndex) { 573 | throw new LogicException(); 574 | } 575 | 576 | $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? []; 577 | $origComments = $origArrayItem->getAttribute(Attribute::COMMENTS) ?? []; 578 | 579 | $commentStartPos = count($origComments) > 0 ? $origComments[0]->startIndex : $itemStartPos; 580 | assert($commentStartPos >= 0); 581 | 582 | $result .= $originalTokens->getContentBetween($tokenIndex, $itemStartPos); 583 | 584 | if (count($delayedAdd) > 0) { 585 | foreach ($delayedAdd as $delayedAddNode) { 586 | $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey]) 587 | && in_array(get_class($delayedAddNode), $this->parenthesesListMap[$mapKey], true); 588 | if ($parenthesesNeeded) { 589 | $result .= '('; 590 | } 591 | 592 | if ($insertNewline) { 593 | $delayedAddComments = $delayedAddNode->getAttribute(Attribute::COMMENTS) ?? []; 594 | if (count($delayedAddComments) > 0) { 595 | $result .= $this->printComments($delayedAddComments, $beforeAsteriskIndent, $afterAsteriskIndent); 596 | $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 597 | } 598 | } 599 | 600 | $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); 601 | if ($parenthesesNeeded) { 602 | $result .= ')'; 603 | } 604 | 605 | if ($insertNewline) { 606 | $result .= $insertStr . sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 607 | } else { 608 | $result .= $insertStr; 609 | } 610 | } 611 | 612 | $delayedAdd = []; 613 | } 614 | 615 | $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey]) 616 | && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true) 617 | && !in_array(get_class($origArrayItem), $this->parenthesesListMap[$mapKey], true); 618 | $addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($itemStartPos, $itemEndPos); 619 | if ($addParentheses) { 620 | $result .= '('; 621 | } 622 | 623 | if ($comments !== $origComments) { 624 | if (count($comments) > 0) { 625 | $result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent); 626 | $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 627 | } 628 | } 629 | 630 | $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens); 631 | if ($addParentheses) { 632 | $result .= ')'; 633 | } 634 | $tokenIndex = $itemEndPos + 1; 635 | 636 | } elseif ($diffType === DiffElem::TYPE_ADD) { 637 | if ($insertStr === null) { 638 | return null; 639 | } 640 | if (!$arrItem instanceof Node) { 641 | return null; 642 | } 643 | 644 | if ($insertStr === ', ' && $isMultiline || count($arrItem->getAttribute(Attribute::COMMENTS) ?? []) > 0) { 645 | $insertStr = ','; 646 | $insertNewline = true; 647 | } 648 | 649 | if ($beforeFirstKeepOrReplace) { 650 | // Will be inserted at the next "replace" or "keep" element 651 | $delayedAdd[] = $arrItem; 652 | continue; 653 | } 654 | 655 | /** @var int $itemEndPos */ 656 | $itemEndPos = $tokenIndex - 1; 657 | if ($insertNewline) { 658 | $comments = $arrItem->getAttribute(Attribute::COMMENTS) ?? []; 659 | $result .= $insertStr; 660 | if (count($comments) > 0) { 661 | $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 662 | $result .= $this->printComments($comments, $beforeAsteriskIndent, $afterAsteriskIndent); 663 | } 664 | $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 665 | } else { 666 | $result .= $insertStr; 667 | } 668 | 669 | $parenthesesNeeded = isset($this->parenthesesListMap[$mapKey]) 670 | && in_array(get_class($arrItem), $this->parenthesesListMap[$mapKey], true); 671 | if ($parenthesesNeeded) { 672 | $result .= '('; 673 | } 674 | 675 | $result .= $this->printNodeFormatPreserving($arrItem, $originalTokens); 676 | if ($parenthesesNeeded) { 677 | $result .= ')'; 678 | } 679 | 680 | $tokenIndex = $itemEndPos + 1; 681 | 682 | } elseif ($diffType === DiffElem::TYPE_REMOVE) { 683 | if (!$origArrayItem instanceof Node) { 684 | return null; 685 | } 686 | 687 | /** @var int $itemStartPos */ 688 | $itemStartPos = $origArrayItem->getAttribute(Attribute::START_INDEX); 689 | 690 | /** @var int $itemEndPos */ 691 | $itemEndPos = $origArrayItem->getAttribute(Attribute::END_INDEX); 692 | if ($itemStartPos < 0 || $itemEndPos < 0) { 693 | throw new LogicException(); 694 | } 695 | 696 | if ($i === 0) { 697 | // If we're removing from the start, keep the tokens before the node and drop those after it, 698 | // instead of the other way around. 699 | $originalTokensArray = $originalTokens->getTokens(); 700 | for ($j = $tokenIndex; $j < $itemStartPos; $j++) { 701 | if ($originalTokensArray[$j][Lexer::TYPE_OFFSET] === Lexer::TOKEN_PHPDOC_EOL) { 702 | break; 703 | } 704 | $result .= $originalTokensArray[$j][Lexer::VALUE_OFFSET]; 705 | } 706 | } 707 | 708 | $tokenIndex = $itemEndPos + 1; 709 | } 710 | } 711 | 712 | if (count($delayedAdd) > 0) { 713 | if (!isset($this->emptyListInsertionMap[$mapKey])) { 714 | return null; 715 | } 716 | 717 | [$findToken, $extraLeft, $extraRight] = $this->emptyListInsertionMap[$mapKey]; 718 | if ($findToken !== null) { 719 | $originalTokensArray = $originalTokens->getTokens(); 720 | for (; $tokenIndex < count($originalTokensArray); $tokenIndex++) { 721 | $result .= $originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET]; 722 | if ($originalTokensArray[$tokenIndex][Lexer::VALUE_OFFSET] !== $findToken) { 723 | continue; 724 | } 725 | 726 | $tokenIndex++; 727 | break; 728 | } 729 | } 730 | $first = true; 731 | $result .= $extraLeft; 732 | foreach ($delayedAdd as $delayedAddNode) { 733 | if (!$first) { 734 | $result .= $insertStr; 735 | if ($insertNewline) { 736 | $result .= sprintf('%s%s*%s', $originalTokens->getDetectedNewline() ?? "\n", $beforeAsteriskIndent, $afterAsteriskIndent); 737 | } 738 | } 739 | 740 | $result .= $this->printNodeFormatPreserving($delayedAddNode, $originalTokens); 741 | $first = false; 742 | } 743 | $result .= $extraRight; 744 | } 745 | 746 | return $result; 747 | } 748 | 749 | /** 750 | * @param list $comments 751 | */ 752 | private function printComments(array $comments, string $beforeAsteriskIndent, string $afterAsteriskIndent): string 753 | { 754 | $formattedComments = []; 755 | 756 | foreach ($comments as $comment) { 757 | $formattedComments[] = str_replace("\n", "\n" . $beforeAsteriskIndent . '*' . $afterAsteriskIndent, $comment->getReformattedText()); 758 | } 759 | 760 | return implode("\n$beforeAsteriskIndent*$afterAsteriskIndent", $formattedComments); 761 | } 762 | 763 | /** 764 | * @param array $nodes 765 | * @return array{bool, string, string} 766 | */ 767 | private function isMultiline(int $initialIndex, array $nodes, TokenIterator $originalTokens): array 768 | { 769 | $isMultiline = count($nodes) > 1; 770 | $pos = $initialIndex; 771 | $allText = ''; 772 | /** @var Node|null $node */ 773 | foreach ($nodes as $node) { 774 | if (!$node instanceof Node) { 775 | continue; 776 | } 777 | 778 | $endPos = $node->getAttribute(Attribute::END_INDEX) + 1; 779 | $text = $originalTokens->getContentBetween($pos, $endPos); 780 | $allText .= $text; 781 | if (strpos($text, "\n") === false) { 782 | // We require that a newline is present between *every* item. If the formatting 783 | // is inconsistent, with only some items having newlines, we don't consider it 784 | // as multiline 785 | $isMultiline = false; 786 | } 787 | $pos = $endPos; 788 | } 789 | 790 | $c = preg_match_all('~\n(?[\\x09\\x20]*)\*(?\\x20*)~', $allText, $matches, PREG_SET_ORDER); 791 | if ($c === 0) { 792 | return [$isMultiline, ' ', ' ']; 793 | } 794 | 795 | $before = ''; 796 | $after = ''; 797 | foreach ($matches as $match) { 798 | if (strlen($match['before']) > strlen($before)) { 799 | $before = $match['before']; 800 | } 801 | if (strlen($match['after']) <= strlen($after)) { 802 | continue; 803 | } 804 | 805 | $after = $match['after']; 806 | } 807 | 808 | $before = strlen($before) === 0 ? ' ' : $before; 809 | $after = strlen($after) === 0 ? ' ' : $after; 810 | 811 | return [$isMultiline, $before, $after]; 812 | } 813 | 814 | private function printNodeFormatPreserving(Node $node, TokenIterator $originalTokens): string 815 | { 816 | /** @var Node|null $originalNode */ 817 | $originalNode = $node->getAttribute(Attribute::ORIGINAL_NODE); 818 | if ($originalNode === null) { 819 | return $this->print($node); 820 | } 821 | 822 | $class = get_class($node); 823 | if ($class !== get_class($originalNode)) { 824 | throw new LogicException(); 825 | } 826 | 827 | $startPos = $originalNode->getAttribute(Attribute::START_INDEX); 828 | $endPos = $originalNode->getAttribute(Attribute::END_INDEX); 829 | if ($startPos < 0 || $endPos < 0) { 830 | throw new LogicException(); 831 | } 832 | 833 | $result = ''; 834 | $pos = $startPos; 835 | $subNodeNames = array_keys(get_object_vars($node)); 836 | foreach ($subNodeNames as $subNodeName) { 837 | $subNode = $node->$subNodeName; 838 | $origSubNode = $originalNode->$subNodeName; 839 | 840 | if ( 841 | (!$subNode instanceof Node && $subNode !== null) 842 | || (!$origSubNode instanceof Node && $origSubNode !== null) 843 | ) { 844 | if ($subNode === $origSubNode) { 845 | // Unchanged, can reuse old code 846 | continue; 847 | } 848 | 849 | if (is_array($subNode) && is_array($origSubNode)) { 850 | // Array subnode changed, we might be able to reconstruct it 851 | $listResult = $this->printArrayFormatPreserving( 852 | $subNode, 853 | $origSubNode, 854 | $originalTokens, 855 | $pos, 856 | $class, 857 | $subNodeName, 858 | ); 859 | 860 | if ($listResult === null) { 861 | return $this->print($node); 862 | } 863 | 864 | $result .= $listResult; 865 | continue; 866 | } 867 | 868 | return $this->print($node); 869 | } 870 | 871 | if ($origSubNode === null) { 872 | if ($subNode === null) { 873 | // Both null, nothing to do 874 | continue; 875 | } 876 | 877 | return $this->print($node); 878 | } 879 | 880 | $subStartPos = $origSubNode->getAttribute(Attribute::START_INDEX); 881 | $subEndPos = $origSubNode->getAttribute(Attribute::END_INDEX); 882 | if ($subStartPos < 0 || $subEndPos < 0) { 883 | throw new LogicException(); 884 | } 885 | 886 | if ($subEndPos < $subStartPos) { 887 | return $this->print($node); 888 | } 889 | 890 | if ($subNode === null) { 891 | return $this->print($node); 892 | } 893 | 894 | $result .= $originalTokens->getContentBetween($pos, $subStartPos); 895 | $mapKey = get_class($node) . '->' . $subNodeName; 896 | $parenthesesNeeded = isset($this->parenthesesMap[$mapKey]) 897 | && in_array(get_class($subNode), $this->parenthesesMap[$mapKey], true); 898 | 899 | if ($subNode->getAttribute(Attribute::ORIGINAL_NODE) !== null) { 900 | $parenthesesNeeded = $parenthesesNeeded 901 | && !in_array(get_class($subNode->getAttribute(Attribute::ORIGINAL_NODE)), $this->parenthesesMap[$mapKey], true); 902 | } 903 | 904 | $addParentheses = $parenthesesNeeded && !$originalTokens->hasParentheses($subStartPos, $subEndPos); 905 | if ($addParentheses) { 906 | $result .= '('; 907 | } 908 | 909 | $result .= $this->printNodeFormatPreserving($subNode, $originalTokens); 910 | if ($addParentheses) { 911 | $result .= ')'; 912 | } 913 | 914 | $pos = $subEndPos + 1; 915 | } 916 | 917 | return $result . $originalTokens->getContentBetween($pos, $endPos + 1); 918 | } 919 | 920 | } 921 | -------------------------------------------------------------------------------- /src/Parser/TypeParser.php: -------------------------------------------------------------------------------- 1 | config = $config; 29 | $this->constExprParser = $constExprParser; 30 | } 31 | 32 | /** @phpstan-impure */ 33 | public function parse(TokenIterator $tokens): Ast\Type\TypeNode 34 | { 35 | $startLine = $tokens->currentTokenLine(); 36 | $startIndex = $tokens->currentTokenIndex(); 37 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { 38 | $type = $this->parseNullable($tokens); 39 | 40 | } else { 41 | $type = $this->parseAtomic($tokens); 42 | 43 | $tokens->pushSavePoint(); 44 | $tokens->skipNewLineTokensAndConsumeComments(); 45 | 46 | try { 47 | $enrichedType = $this->enrichTypeOnUnionOrIntersection($tokens, $type); 48 | 49 | } catch (ParserException $parserException) { 50 | $enrichedType = null; 51 | } 52 | 53 | if ($enrichedType !== null) { 54 | $type = $enrichedType; 55 | $tokens->dropSavePoint(); 56 | 57 | } else { 58 | $tokens->rollback(); 59 | $type = $this->enrichTypeOnUnionOrIntersection($tokens, $type) ?? $type; 60 | } 61 | } 62 | 63 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 64 | } 65 | 66 | /** @phpstan-impure */ 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 | * @internal 83 | * @template T of Ast\Node 84 | * @param T $type 85 | * @return T 86 | */ 87 | public function enrichWithAttributes(TokenIterator $tokens, Ast\Node $type, int $startLine, int $startIndex): Ast\Node 88 | { 89 | if ($this->config->useLinesAttributes) { 90 | $type->setAttribute(Ast\Attribute::START_LINE, $startLine); 91 | $type->setAttribute(Ast\Attribute::END_LINE, $tokens->currentTokenLine()); 92 | } 93 | 94 | $comments = $tokens->flushComments(); 95 | if ($this->config->useCommentsAttributes) { 96 | $type->setAttribute(Ast\Attribute::COMMENTS, $comments); 97 | } 98 | 99 | if ($this->config->useIndexAttributes) { 100 | $type->setAttribute(Ast\Attribute::START_INDEX, $startIndex); 101 | $type->setAttribute(Ast\Attribute::END_INDEX, $tokens->endIndexOfLastRelevantToken()); 102 | } 103 | 104 | return $type; 105 | } 106 | 107 | /** @phpstan-impure */ 108 | private function subParse(TokenIterator $tokens): Ast\Type\TypeNode 109 | { 110 | $startLine = $tokens->currentTokenLine(); 111 | $startIndex = $tokens->currentTokenIndex(); 112 | 113 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { 114 | $type = $this->parseNullable($tokens); 115 | 116 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { 117 | $type = $this->parseConditionalForParameter($tokens, $tokens->currentTokenValue()); 118 | 119 | } else { 120 | $type = $this->parseAtomic($tokens); 121 | 122 | if ($tokens->isCurrentTokenValue('is')) { 123 | $type = $this->parseConditional($tokens, $type); 124 | } else { 125 | $tokens->skipNewLineTokensAndConsumeComments(); 126 | 127 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { 128 | $type = $this->subParseUnion($tokens, $type); 129 | 130 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { 131 | $type = $this->subParseIntersection($tokens, $type); 132 | } 133 | } 134 | } 135 | 136 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 137 | } 138 | 139 | /** @phpstan-impure */ 140 | private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode 141 | { 142 | $startLine = $tokens->currentTokenLine(); 143 | $startIndex = $tokens->currentTokenIndex(); 144 | 145 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { 146 | $tokens->skipNewLineTokensAndConsumeComments(); 147 | $type = $this->subParse($tokens); 148 | $tokens->skipNewLineTokensAndConsumeComments(); 149 | 150 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); 151 | 152 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 153 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 154 | } 155 | 156 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 157 | } 158 | 159 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { 160 | $type = $this->enrichWithAttributes($tokens, new Ast\Type\ThisTypeNode(), $startLine, $startIndex); 161 | 162 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 163 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 164 | } 165 | 166 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 167 | } 168 | 169 | $currentTokenValue = $tokens->currentTokenValue(); 170 | $tokens->pushSavePoint(); // because of ConstFetchNode 171 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { 172 | $type = $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode($currentTokenValue), $startLine, $startIndex); 173 | 174 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { 175 | $tokens->dropSavePoint(); // because of ConstFetchNode 176 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { 177 | $tokens->pushSavePoint(); 178 | 179 | $isHtml = $this->isHtml($tokens); 180 | $tokens->rollback(); 181 | if ($isHtml) { 182 | return $type; 183 | } 184 | 185 | $origType = $type; 186 | $type = $this->tryParseCallable($tokens, $type, true); 187 | if ($type === $origType) { 188 | $type = $this->parseGeneric($tokens, $type); 189 | 190 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 191 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 192 | } 193 | } 194 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { 195 | $type = $this->tryParseCallable($tokens, $type, false); 196 | 197 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 198 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 199 | 200 | } elseif (in_array($type->name, [ 201 | Ast\Type\ArrayShapeNode::KIND_ARRAY, 202 | Ast\Type\ArrayShapeNode::KIND_LIST, 203 | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, 204 | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, 205 | 'object', 206 | ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { 207 | if ($type->name === 'object') { 208 | $type = $this->parseObjectShape($tokens); 209 | } else { 210 | $type = $this->parseArrayShape($tokens, $type, $type->name); 211 | } 212 | 213 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 214 | $type = $this->tryParseArrayOrOffsetAccess( 215 | $tokens, 216 | $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex), 217 | ); 218 | } 219 | } 220 | 221 | return $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 222 | } else { 223 | $tokens->rollback(); // because of ConstFetchNode 224 | } 225 | } else { 226 | $tokens->dropSavePoint(); // because of ConstFetchNode 227 | } 228 | 229 | $currentTokenValue = $tokens->currentTokenValue(); 230 | $currentTokenType = $tokens->currentTokenType(); 231 | $currentTokenOffset = $tokens->currentTokenOffset(); 232 | $currentTokenLine = $tokens->currentTokenLine(); 233 | 234 | try { 235 | $constExpr = $this->constExprParser->parse($tokens); 236 | if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { 237 | throw new ParserException( 238 | $currentTokenValue, 239 | $currentTokenType, 240 | $currentTokenOffset, 241 | Lexer::TOKEN_IDENTIFIER, 242 | null, 243 | $currentTokenLine, 244 | ); 245 | } 246 | 247 | $type = $this->enrichWithAttributes( 248 | $tokens, 249 | new Ast\Type\ConstTypeNode($constExpr), 250 | $startLine, 251 | $startIndex, 252 | ); 253 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 254 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 255 | } 256 | 257 | return $type; 258 | } catch (LogicException $e) { 259 | throw new ParserException( 260 | $currentTokenValue, 261 | $currentTokenType, 262 | $currentTokenOffset, 263 | Lexer::TOKEN_IDENTIFIER, 264 | null, 265 | $currentTokenLine, 266 | ); 267 | } 268 | } 269 | 270 | /** @phpstan-impure */ 271 | private function parseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode 272 | { 273 | $types = [$type]; 274 | 275 | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { 276 | $types[] = $this->parseAtomic($tokens); 277 | $tokens->pushSavePoint(); 278 | $tokens->skipNewLineTokensAndConsumeComments(); 279 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_UNION)) { 280 | $tokens->rollback(); 281 | break; 282 | } 283 | 284 | $tokens->dropSavePoint(); 285 | } 286 | 287 | return new Ast\Type\UnionTypeNode($types); 288 | } 289 | 290 | /** @phpstan-impure */ 291 | private function subParseUnion(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode 292 | { 293 | $types = [$type]; 294 | 295 | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_UNION)) { 296 | $tokens->skipNewLineTokensAndConsumeComments(); 297 | $types[] = $this->parseAtomic($tokens); 298 | $tokens->skipNewLineTokensAndConsumeComments(); 299 | } 300 | 301 | return new Ast\Type\UnionTypeNode($types); 302 | } 303 | 304 | /** @phpstan-impure */ 305 | private function parseIntersection(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode 306 | { 307 | $types = [$type]; 308 | 309 | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_INTERSECTION)) { 310 | $types[] = $this->parseAtomic($tokens); 311 | $tokens->pushSavePoint(); 312 | $tokens->skipNewLineTokensAndConsumeComments(); 313 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_INTERSECTION)) { 314 | $tokens->rollback(); 315 | break; 316 | } 317 | 318 | $tokens->dropSavePoint(); 319 | } 320 | 321 | return new Ast\Type\IntersectionTypeNode($types); 322 | } 323 | 324 | /** @phpstan-impure */ 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->skipNewLineTokensAndConsumeComments(); 331 | $types[] = $this->parseAtomic($tokens); 332 | $tokens->skipNewLineTokensAndConsumeComments(); 333 | } 334 | 335 | return new Ast\Type\IntersectionTypeNode($types); 336 | } 337 | 338 | /** @phpstan-impure */ 339 | private function parseConditional(TokenIterator $tokens, Ast\Type\TypeNode $subjectType): Ast\Type\TypeNode 340 | { 341 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 342 | 343 | $negated = false; 344 | if ($tokens->isCurrentTokenValue('not')) { 345 | $negated = true; 346 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 347 | } 348 | 349 | $targetType = $this->parse($tokens); 350 | 351 | $tokens->skipNewLineTokensAndConsumeComments(); 352 | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); 353 | $tokens->skipNewLineTokensAndConsumeComments(); 354 | 355 | $ifType = $this->parse($tokens); 356 | 357 | $tokens->skipNewLineTokensAndConsumeComments(); 358 | $tokens->consumeTokenType(Lexer::TOKEN_COLON); 359 | $tokens->skipNewLineTokensAndConsumeComments(); 360 | 361 | $elseType = $this->subParse($tokens); 362 | 363 | return new Ast\Type\ConditionalTypeNode($subjectType, $targetType, $ifType, $elseType, $negated); 364 | } 365 | 366 | /** @phpstan-impure */ 367 | private function parseConditionalForParameter(TokenIterator $tokens, string $parameterName): Ast\Type\TypeNode 368 | { 369 | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); 370 | $tokens->consumeTokenValue(Lexer::TOKEN_IDENTIFIER, 'is'); 371 | 372 | $negated = false; 373 | if ($tokens->isCurrentTokenValue('not')) { 374 | $negated = true; 375 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 376 | } 377 | 378 | $targetType = $this->parse($tokens); 379 | 380 | $tokens->skipNewLineTokensAndConsumeComments(); 381 | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); 382 | $tokens->skipNewLineTokensAndConsumeComments(); 383 | 384 | $ifType = $this->parse($tokens); 385 | 386 | $tokens->skipNewLineTokensAndConsumeComments(); 387 | $tokens->consumeTokenType(Lexer::TOKEN_COLON); 388 | $tokens->skipNewLineTokensAndConsumeComments(); 389 | 390 | $elseType = $this->subParse($tokens); 391 | 392 | return new Ast\Type\ConditionalTypeForParameterNode($parameterName, $targetType, $ifType, $elseType, $negated); 393 | } 394 | 395 | /** @phpstan-impure */ 396 | private function parseNullable(TokenIterator $tokens): Ast\Type\TypeNode 397 | { 398 | $tokens->consumeTokenType(Lexer::TOKEN_NULLABLE); 399 | 400 | $type = $this->parseAtomic($tokens); 401 | 402 | return new Ast\Type\NullableTypeNode($type); 403 | } 404 | 405 | /** @phpstan-impure */ 406 | public function isHtml(TokenIterator $tokens): bool 407 | { 408 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); 409 | 410 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_IDENTIFIER)) { 411 | return false; 412 | } 413 | 414 | $htmlTagName = $tokens->currentTokenValue(); 415 | 416 | $tokens->next(); 417 | 418 | if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { 419 | return false; 420 | } 421 | 422 | $endTag = ''; 423 | $endTagSearchOffset = - strlen($endTag); 424 | 425 | while (!$tokens->isCurrentTokenType(Lexer::TOKEN_END)) { 426 | if ( 427 | ( 428 | $tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET) 429 | && strpos($tokens->currentTokenValue(), '/' . $htmlTagName . '>') !== false 430 | ) 431 | || substr_compare($tokens->currentTokenValue(), $endTag, $endTagSearchOffset) === 0 432 | ) { 433 | return true; 434 | } 435 | 436 | $tokens->next(); 437 | } 438 | 439 | return false; 440 | } 441 | 442 | /** @phpstan-impure */ 443 | public function parseGeneric(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $baseType): Ast\Type\GenericTypeNode 444 | { 445 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); 446 | $tokens->skipNewLineTokensAndConsumeComments(); 447 | 448 | $startLine = $baseType->getAttribute(Ast\Attribute::START_LINE); 449 | $startIndex = $baseType->getAttribute(Ast\Attribute::START_INDEX); 450 | $genericTypes = []; 451 | $variances = []; 452 | 453 | $isFirst = true; 454 | while ( 455 | $isFirst 456 | || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA) 457 | ) { 458 | $tokens->skipNewLineTokensAndConsumeComments(); 459 | 460 | // trailing comma case 461 | if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { 462 | break; 463 | } 464 | $isFirst = false; 465 | 466 | [$genericTypes[], $variances[]] = $this->parseGenericTypeArgument($tokens); 467 | $tokens->skipNewLineTokensAndConsumeComments(); 468 | } 469 | 470 | $type = new Ast\Type\GenericTypeNode($baseType, $genericTypes, $variances); 471 | if ($startLine !== null && $startIndex !== null) { 472 | $type = $this->enrichWithAttributes($tokens, $type, $startLine, $startIndex); 473 | } 474 | 475 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); 476 | 477 | return $type; 478 | } 479 | 480 | /** 481 | * @phpstan-impure 482 | * @return array{Ast\Type\TypeNode, Ast\Type\GenericTypeNode::VARIANCE_*} 483 | */ 484 | public function parseGenericTypeArgument(TokenIterator $tokens): array 485 | { 486 | $startLine = $tokens->currentTokenLine(); 487 | $startIndex = $tokens->currentTokenIndex(); 488 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_WILDCARD)) { 489 | return [ 490 | $this->enrichWithAttributes($tokens, new Ast\Type\IdentifierTypeNode('mixed'), $startLine, $startIndex), 491 | Ast\Type\GenericTypeNode::VARIANCE_BIVARIANT, 492 | ]; 493 | } 494 | 495 | if ($tokens->tryConsumeTokenValue('contravariant')) { 496 | $variance = Ast\Type\GenericTypeNode::VARIANCE_CONTRAVARIANT; 497 | } elseif ($tokens->tryConsumeTokenValue('covariant')) { 498 | $variance = Ast\Type\GenericTypeNode::VARIANCE_COVARIANT; 499 | } else { 500 | $variance = Ast\Type\GenericTypeNode::VARIANCE_INVARIANT; 501 | } 502 | 503 | $type = $this->parse($tokens); 504 | return [$type, $variance]; 505 | } 506 | 507 | /** 508 | * @throws ParserException 509 | * @param ?callable(TokenIterator): string $parseDescription 510 | */ 511 | public function parseTemplateTagValue( 512 | TokenIterator $tokens, 513 | ?callable $parseDescription = null 514 | ): TemplateTagValueNode 515 | { 516 | $name = $tokens->currentTokenValue(); 517 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 518 | 519 | $upperBound = $lowerBound = null; 520 | 521 | if ($tokens->tryConsumeTokenValue('of') || $tokens->tryConsumeTokenValue('as')) { 522 | $upperBound = $this->parse($tokens); 523 | } 524 | 525 | if ($tokens->tryConsumeTokenValue('super')) { 526 | $lowerBound = $this->parse($tokens); 527 | } 528 | 529 | if ($tokens->tryConsumeTokenValue('=')) { 530 | $default = $this->parse($tokens); 531 | } else { 532 | $default = null; 533 | } 534 | 535 | if ($parseDescription !== null) { 536 | $description = $parseDescription($tokens); 537 | } else { 538 | $description = ''; 539 | } 540 | 541 | if ($name === '') { 542 | throw new LogicException('Template tag name cannot be empty.'); 543 | } 544 | 545 | return new Ast\PhpDoc\TemplateTagValueNode($name, $upperBound, $description, $default, $lowerBound); 546 | } 547 | 548 | /** @phpstan-impure */ 549 | private function parseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode 550 | { 551 | $templates = $hasTemplate 552 | ? $this->parseCallableTemplates($tokens) 553 | : []; 554 | 555 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES); 556 | $tokens->skipNewLineTokensAndConsumeComments(); 557 | 558 | $parameters = []; 559 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { 560 | $parameters[] = $this->parseCallableParameter($tokens); 561 | $tokens->skipNewLineTokensAndConsumeComments(); 562 | while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { 563 | $tokens->skipNewLineTokensAndConsumeComments(); 564 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_PARENTHESES)) { 565 | break; 566 | } 567 | $parameters[] = $this->parseCallableParameter($tokens); 568 | $tokens->skipNewLineTokensAndConsumeComments(); 569 | } 570 | } 571 | 572 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); 573 | $tokens->consumeTokenType(Lexer::TOKEN_COLON); 574 | 575 | $startLine = $tokens->currentTokenLine(); 576 | $startIndex = $tokens->currentTokenIndex(); 577 | $returnType = $this->enrichWithAttributes($tokens, $this->parseCallableReturnType($tokens), $startLine, $startIndex); 578 | 579 | return new Ast\Type\CallableTypeNode($identifier, $parameters, $returnType, $templates); 580 | } 581 | 582 | /** 583 | * @return Ast\PhpDoc\TemplateTagValueNode[] 584 | * 585 | * @phpstan-impure 586 | */ 587 | private function parseCallableTemplates(TokenIterator $tokens): array 588 | { 589 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); 590 | 591 | $templates = []; 592 | 593 | $isFirst = true; 594 | while ($isFirst || $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { 595 | $tokens->skipNewLineTokensAndConsumeComments(); 596 | 597 | // trailing comma case 598 | if (!$isFirst && $tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET)) { 599 | break; 600 | } 601 | $isFirst = false; 602 | 603 | $templates[] = $this->parseCallableTemplateArgument($tokens); 604 | $tokens->skipNewLineTokensAndConsumeComments(); 605 | } 606 | 607 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); 608 | 609 | return $templates; 610 | } 611 | 612 | private function parseCallableTemplateArgument(TokenIterator $tokens): Ast\PhpDoc\TemplateTagValueNode 613 | { 614 | $startLine = $tokens->currentTokenLine(); 615 | $startIndex = $tokens->currentTokenIndex(); 616 | 617 | return $this->enrichWithAttributes( 618 | $tokens, 619 | $this->parseTemplateTagValue($tokens), 620 | $startLine, 621 | $startIndex, 622 | ); 623 | } 624 | 625 | /** @phpstan-impure */ 626 | private function parseCallableParameter(TokenIterator $tokens): Ast\Type\CallableTypeParameterNode 627 | { 628 | $startLine = $tokens->currentTokenLine(); 629 | $startIndex = $tokens->currentTokenIndex(); 630 | $type = $this->parse($tokens); 631 | $isReference = $tokens->tryConsumeTokenType(Lexer::TOKEN_REFERENCE); 632 | $isVariadic = $tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC); 633 | 634 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_VARIABLE)) { 635 | $parameterName = $tokens->currentTokenValue(); 636 | $tokens->consumeTokenType(Lexer::TOKEN_VARIABLE); 637 | 638 | } else { 639 | $parameterName = ''; 640 | } 641 | 642 | $isOptional = $tokens->tryConsumeTokenType(Lexer::TOKEN_EQUAL); 643 | return $this->enrichWithAttributes( 644 | $tokens, 645 | new Ast\Type\CallableTypeParameterNode($type, $isReference, $isVariadic, $parameterName, $isOptional), 646 | $startLine, 647 | $startIndex, 648 | ); 649 | } 650 | 651 | /** @phpstan-impure */ 652 | private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNode 653 | { 654 | $startLine = $tokens->currentTokenLine(); 655 | $startIndex = $tokens->currentTokenIndex(); 656 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_NULLABLE)) { 657 | return $this->parseNullable($tokens); 658 | 659 | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_PARENTHESES)) { 660 | $type = $this->subParse($tokens); 661 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_PARENTHESES); 662 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 663 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 664 | } 665 | 666 | return $type; 667 | } elseif ($tokens->tryConsumeTokenType(Lexer::TOKEN_THIS_VARIABLE)) { 668 | $type = new Ast\Type\ThisTypeNode(); 669 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 670 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( 671 | $tokens, 672 | $type, 673 | $startLine, 674 | $startIndex, 675 | )); 676 | } 677 | 678 | return $type; 679 | } else { 680 | $currentTokenValue = $tokens->currentTokenValue(); 681 | $tokens->pushSavePoint(); // because of ConstFetchNode 682 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_IDENTIFIER)) { 683 | $type = new Ast\Type\IdentifierTypeNode($currentTokenValue); 684 | 685 | if (!$tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_COLON)) { 686 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { 687 | $type = $this->parseGeneric( 688 | $tokens, 689 | $this->enrichWithAttributes( 690 | $tokens, 691 | $type, 692 | $startLine, 693 | $startIndex, 694 | ), 695 | ); 696 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 697 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( 698 | $tokens, 699 | $type, 700 | $startLine, 701 | $startIndex, 702 | )); 703 | } 704 | 705 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 706 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( 707 | $tokens, 708 | $type, 709 | $startLine, 710 | $startIndex, 711 | )); 712 | 713 | } elseif (in_array($type->name, [ 714 | Ast\Type\ArrayShapeNode::KIND_ARRAY, 715 | Ast\Type\ArrayShapeNode::KIND_LIST, 716 | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, 717 | Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, 718 | 'object', 719 | ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { 720 | if ($type->name === 'object') { 721 | $type = $this->parseObjectShape($tokens); 722 | } else { 723 | $type = $this->parseArrayShape($tokens, $this->enrichWithAttributes( 724 | $tokens, 725 | $type, 726 | $startLine, 727 | $startIndex, 728 | ), $type->name); 729 | } 730 | 731 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 732 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $this->enrichWithAttributes( 733 | $tokens, 734 | $type, 735 | $startLine, 736 | $startIndex, 737 | )); 738 | } 739 | } 740 | 741 | return $type; 742 | } else { 743 | $tokens->rollback(); // because of ConstFetchNode 744 | } 745 | } else { 746 | $tokens->dropSavePoint(); // because of ConstFetchNode 747 | } 748 | } 749 | 750 | $currentTokenValue = $tokens->currentTokenValue(); 751 | $currentTokenType = $tokens->currentTokenType(); 752 | $currentTokenOffset = $tokens->currentTokenOffset(); 753 | $currentTokenLine = $tokens->currentTokenLine(); 754 | 755 | try { 756 | $constExpr = $this->constExprParser->parse($tokens); 757 | if ($constExpr instanceof Ast\ConstExpr\ConstExprArrayNode) { 758 | throw new ParserException( 759 | $currentTokenValue, 760 | $currentTokenType, 761 | $currentTokenOffset, 762 | Lexer::TOKEN_IDENTIFIER, 763 | null, 764 | $currentTokenLine, 765 | ); 766 | } 767 | 768 | $type = $this->enrichWithAttributes( 769 | $tokens, 770 | new Ast\Type\ConstTypeNode($constExpr), 771 | $startLine, 772 | $startIndex, 773 | ); 774 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 775 | $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); 776 | } 777 | 778 | return $type; 779 | } catch (LogicException $e) { 780 | throw new ParserException( 781 | $currentTokenValue, 782 | $currentTokenType, 783 | $currentTokenOffset, 784 | Lexer::TOKEN_IDENTIFIER, 785 | null, 786 | $currentTokenLine, 787 | ); 788 | } 789 | } 790 | 791 | /** @phpstan-impure */ 792 | private function tryParseCallable(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $identifier, bool $hasTemplate): Ast\Type\TypeNode 793 | { 794 | try { 795 | $tokens->pushSavePoint(); 796 | $type = $this->parseCallable($tokens, $identifier, $hasTemplate); 797 | $tokens->dropSavePoint(); 798 | 799 | } catch (ParserException $e) { 800 | $tokens->rollback(); 801 | $type = $identifier; 802 | } 803 | 804 | return $type; 805 | } 806 | 807 | /** @phpstan-impure */ 808 | private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode 809 | { 810 | $startLine = $type->getAttribute(Ast\Attribute::START_LINE); 811 | $startIndex = $type->getAttribute(Ast\Attribute::START_INDEX); 812 | try { 813 | while ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { 814 | $tokens->pushSavePoint(); 815 | 816 | $canBeOffsetAccessType = !$tokens->isPrecededByHorizontalWhitespace(); 817 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET); 818 | 819 | if ($canBeOffsetAccessType && !$tokens->isCurrentTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET)) { 820 | $offset = $this->parse($tokens); 821 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); 822 | $tokens->dropSavePoint(); 823 | $type = new Ast\Type\OffsetAccessTypeNode($type, $offset); 824 | 825 | if ($startLine !== null && $startIndex !== null) { 826 | $type = $this->enrichWithAttributes( 827 | $tokens, 828 | $type, 829 | $startLine, 830 | $startIndex, 831 | ); 832 | } 833 | } else { 834 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_SQUARE_BRACKET); 835 | $tokens->dropSavePoint(); 836 | $type = new Ast\Type\ArrayTypeNode($type); 837 | 838 | if ($startLine !== null && $startIndex !== null) { 839 | $type = $this->enrichWithAttributes( 840 | $tokens, 841 | $type, 842 | $startLine, 843 | $startIndex, 844 | ); 845 | } 846 | } 847 | } 848 | 849 | } catch (ParserException $e) { 850 | $tokens->rollback(); 851 | } 852 | 853 | return $type; 854 | } 855 | 856 | /** 857 | * @phpstan-impure 858 | * @param Ast\Type\ArrayShapeNode::KIND_* $kind 859 | */ 860 | private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, string $kind): Ast\Type\ArrayShapeNode 861 | { 862 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); 863 | 864 | $items = []; 865 | $sealed = true; 866 | $unsealedType = null; 867 | 868 | $done = false; 869 | 870 | do { 871 | $tokens->skipNewLineTokensAndConsumeComments(); 872 | 873 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { 874 | return Ast\Type\ArrayShapeNode::createSealed($items, $kind); 875 | } 876 | 877 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { 878 | $sealed = false; 879 | 880 | $tokens->skipNewLineTokensAndConsumeComments(); 881 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { 882 | if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { 883 | $unsealedType = $this->parseArrayShapeUnsealedType($tokens); 884 | } else { 885 | $unsealedType = $this->parseListShapeUnsealedType($tokens); 886 | } 887 | $tokens->skipNewLineTokensAndConsumeComments(); 888 | } 889 | 890 | $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); 891 | break; 892 | } 893 | 894 | $items[] = $this->parseArrayShapeItem($tokens); 895 | $tokens->skipNewLineTokensAndConsumeComments(); 896 | if (!$tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { 897 | $done = true; 898 | } 899 | if ($tokens->currentTokenType() !== Lexer::TOKEN_COMMENT) { 900 | continue; 901 | } 902 | 903 | $tokens->next(); 904 | 905 | } while (!$done); 906 | 907 | $tokens->skipNewLineTokensAndConsumeComments(); 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 | /** @phpstan-impure */ 918 | private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode 919 | { 920 | $startLine = $tokens->currentTokenLine(); 921 | $startIndex = $tokens->currentTokenIndex(); 922 | 923 | // parse any comments above the item 924 | $tokens->skipNewLineTokensAndConsumeComments(); 925 | 926 | try { 927 | $tokens->pushSavePoint(); 928 | $key = $this->parseArrayShapeKey($tokens); 929 | $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); 930 | $tokens->consumeTokenType(Lexer::TOKEN_COLON); 931 | $value = $this->parse($tokens); 932 | 933 | $tokens->dropSavePoint(); 934 | 935 | return $this->enrichWithAttributes( 936 | $tokens, 937 | new Ast\Type\ArrayShapeItemNode($key, $optional, $value), 938 | $startLine, 939 | $startIndex, 940 | ); 941 | } catch (ParserException $e) { 942 | $tokens->rollback(); 943 | $value = $this->parse($tokens); 944 | 945 | return $this->enrichWithAttributes( 946 | $tokens, 947 | new Ast\Type\ArrayShapeItemNode(null, false, $value), 948 | $startLine, 949 | $startIndex, 950 | ); 951 | } 952 | } 953 | 954 | /** 955 | * @phpstan-impure 956 | * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\ConstExpr\ConstFetchNode|Ast\Type\IdentifierTypeNode 957 | */ 958 | private function parseArrayShapeKey(TokenIterator $tokens) 959 | { 960 | $startIndex = $tokens->currentTokenIndex(); 961 | $startLine = $tokens->currentTokenLine(); 962 | 963 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { 964 | $key = new Ast\ConstExpr\ConstExprIntegerNode(str_replace('_', '', $tokens->currentTokenValue())); 965 | $tokens->next(); 966 | 967 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { 968 | $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED); 969 | $tokens->next(); 970 | 971 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { 972 | $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED); 973 | 974 | $tokens->next(); 975 | 976 | } else { 977 | $identifier = $tokens->currentTokenValue(); 978 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 979 | 980 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_DOUBLE_COLON)) { 981 | $classConstantName = $tokens->currentTokenValue(); 982 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 983 | 984 | $key = new Ast\ConstExpr\ConstFetchNode($identifier, $classConstantName); 985 | } else { 986 | $key = new Ast\Type\IdentifierTypeNode($identifier); 987 | } 988 | } 989 | 990 | return $this->enrichWithAttributes( 991 | $tokens, 992 | $key, 993 | $startLine, 994 | $startIndex, 995 | ); 996 | } 997 | 998 | /** 999 | * @phpstan-impure 1000 | */ 1001 | private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode 1002 | { 1003 | $startLine = $tokens->currentTokenLine(); 1004 | $startIndex = $tokens->currentTokenIndex(); 1005 | 1006 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); 1007 | $tokens->skipNewLineTokensAndConsumeComments(); 1008 | 1009 | $valueType = $this->parse($tokens); 1010 | $tokens->skipNewLineTokensAndConsumeComments(); 1011 | 1012 | $keyType = null; 1013 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { 1014 | $tokens->skipNewLineTokensAndConsumeComments(); 1015 | 1016 | $keyType = $valueType; 1017 | $valueType = $this->parse($tokens); 1018 | $tokens->skipNewLineTokensAndConsumeComments(); 1019 | } 1020 | 1021 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); 1022 | 1023 | return $this->enrichWithAttributes( 1024 | $tokens, 1025 | new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType), 1026 | $startLine, 1027 | $startIndex, 1028 | ); 1029 | } 1030 | 1031 | /** 1032 | * @phpstan-impure 1033 | */ 1034 | private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode 1035 | { 1036 | $startLine = $tokens->currentTokenLine(); 1037 | $startIndex = $tokens->currentTokenIndex(); 1038 | 1039 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); 1040 | $tokens->skipNewLineTokensAndConsumeComments(); 1041 | 1042 | $valueType = $this->parse($tokens); 1043 | $tokens->skipNewLineTokensAndConsumeComments(); 1044 | 1045 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); 1046 | 1047 | return $this->enrichWithAttributes( 1048 | $tokens, 1049 | new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null), 1050 | $startLine, 1051 | $startIndex, 1052 | ); 1053 | } 1054 | 1055 | /** 1056 | * @phpstan-impure 1057 | */ 1058 | private function parseObjectShape(TokenIterator $tokens): Ast\Type\ObjectShapeNode 1059 | { 1060 | $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); 1061 | 1062 | $items = []; 1063 | 1064 | do { 1065 | $tokens->skipNewLineTokensAndConsumeComments(); 1066 | 1067 | if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { 1068 | return new Ast\Type\ObjectShapeNode($items); 1069 | } 1070 | 1071 | $items[] = $this->parseObjectShapeItem($tokens); 1072 | 1073 | $tokens->skipNewLineTokensAndConsumeComments(); 1074 | } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); 1075 | 1076 | $tokens->skipNewLineTokensAndConsumeComments(); 1077 | $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); 1078 | 1079 | return new Ast\Type\ObjectShapeNode($items); 1080 | } 1081 | 1082 | /** @phpstan-impure */ 1083 | private function parseObjectShapeItem(TokenIterator $tokens): Ast\Type\ObjectShapeItemNode 1084 | { 1085 | $startLine = $tokens->currentTokenLine(); 1086 | $startIndex = $tokens->currentTokenIndex(); 1087 | 1088 | $tokens->skipNewLineTokensAndConsumeComments(); 1089 | 1090 | $key = $this->parseObjectShapeKey($tokens); 1091 | $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); 1092 | $tokens->consumeTokenType(Lexer::TOKEN_COLON); 1093 | $value = $this->parse($tokens); 1094 | 1095 | return $this->enrichWithAttributes( 1096 | $tokens, 1097 | new Ast\Type\ObjectShapeItemNode($key, $optional, $value), 1098 | $startLine, 1099 | $startIndex, 1100 | ); 1101 | } 1102 | 1103 | /** 1104 | * @phpstan-impure 1105 | * @return Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode 1106 | */ 1107 | private function parseObjectShapeKey(TokenIterator $tokens) 1108 | { 1109 | $startLine = $tokens->currentTokenLine(); 1110 | $startIndex = $tokens->currentTokenIndex(); 1111 | 1112 | if ($tokens->isCurrentTokenType(Lexer::TOKEN_SINGLE_QUOTED_STRING)) { 1113 | $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::SINGLE_QUOTED); 1114 | $tokens->next(); 1115 | 1116 | } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_DOUBLE_QUOTED_STRING)) { 1117 | $key = new Ast\ConstExpr\ConstExprStringNode(StringUnescaper::unescapeString($tokens->currentTokenValue()), Ast\ConstExpr\ConstExprStringNode::DOUBLE_QUOTED); 1118 | $tokens->next(); 1119 | 1120 | } else { 1121 | $key = new Ast\Type\IdentifierTypeNode($tokens->currentTokenValue()); 1122 | $tokens->consumeTokenType(Lexer::TOKEN_IDENTIFIER); 1123 | } 1124 | 1125 | return $this->enrichWithAttributes($tokens, $key, $startLine, $startIndex); 1126 | } 1127 | 1128 | } 1129 | --------------------------------------------------------------------------------