diff --git a/.editorconfig b/.editorconfig index 5d66bc42..07a06ef0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,3 +25,7 @@ indent_size = 2 [composer.json] indent_style = tab indent_size = 4 + +[*.abnf] +indent_style = tab +indent_size = 4 diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 5af5be1f..571c225b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -60,11 +60,23 @@ Array = 1*(TokenSquareBracketOpen TokenSquareBracketClose) ArrayShape - = TokenCurlyBracketOpen ArrayShapeItem *(TokenComma ArrayShapeItem) TokenCurlyBracketClose + = Shape ArrayShapeItem - = (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type - / Type + = ShapeItem + +Shape + = TokenCurlyBracketOpen ShapeItem *(TokenComma ShapeItem) TokenCurlyBracketClose + +ShapeItem + = (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type + / Type + +ObjectShape + = Shape + +ObjectShapeItem + = ShapeItem ; ---------------------------------------------------------------------------- ; ; ConstantExpr ; diff --git a/src/Ast/Type/ArrayShapeItemNode.php b/src/Ast/Type/ArrayShapeItemNode.php index 660c6c9d..16a2a84b 100644 --- a/src/Ast/Type/ArrayShapeItemNode.php +++ b/src/Ast/Type/ArrayShapeItemNode.php @@ -2,48 +2,7 @@ namespace PHPStan\PhpDocParser\Ast\Type; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode; -use PHPStan\PhpDocParser\Ast\NodeAttributes; -use function sprintf; - -class ArrayShapeItemNode implements TypeNode +class ArrayShapeItemNode extends ShapeItemNode implements TypeNode { - use NodeAttributes; - - /** @var ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null */ - public $keyName; - - /** @var bool */ - public $optional; - - /** @var TypeNode */ - public $valueType; - - /** - * @param ConstExprIntegerNode|ConstExprStringNode|IdentifierTypeNode|null $keyName - */ - public function __construct($keyName, bool $optional, TypeNode $valueType) - { - $this->keyName = $keyName; - $this->optional = $optional; - $this->valueType = $valueType; - } - - - public function __toString(): string - { - if ($this->keyName !== null) { - return sprintf( - '%s%s: %s', - (string) $this->keyName, - $this->optional ? '?' : '', - (string) $this->valueType - ); - } - - return (string) $this->valueType; - } - } diff --git a/src/Ast/Type/ObjectShapeItemNode.php b/src/Ast/Type/ObjectShapeItemNode.php new file mode 100644 index 00000000..4d92df8a --- /dev/null +++ b/src/Ast/Type/ObjectShapeItemNode.php @@ -0,0 +1,8 @@ +identifier = $identifier; + $this->items = $items; + } + + + public function __toString(): string + { + return "{$this->identifier}{" . implode(', ', $this->items) . '}'; + } + +} diff --git a/src/Ast/Type/ShapeItemNode.php b/src/Ast/Type/ShapeItemNode.php new file mode 100644 index 00000000..e09b7aa7 --- /dev/null +++ b/src/Ast/Type/ShapeItemNode.php @@ -0,0 +1,49 @@ +keyName = $keyName; + $this->optional = $optional; + $this->valueType = $valueType; + } + + + public function __toString(): string + { + if ($this->keyName !== null) { + return sprintf( + '%s%s: %s', + (string) $this->keyName, + $this->optional ? '?' : '', + (string) $this->valueType + ); + } + + return (string) $this->valueType; + } + +} diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 170b2581..3e18cc81 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -129,6 +129,8 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); } + } elseif ($type->name === 'object' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + $type = $this->parseObjectShape($tokens, $type); } return $type; @@ -468,53 +470,76 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ return $type; } - /** @phpstan-impure */ - private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode + private function parseShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): array { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - return new Ast\Type\ArrayShapeNode([]); + return []; } $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $items = [$this->parseArrayShapeItem($tokens)]; + $items = [$this->parseShapeItem($tokens, $type)]; $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { // trailing comma case - return new Ast\Type\ArrayShapeNode($items); + return $items; } - $items[] = $this->parseArrayShapeItem($tokens); + $items[] = $this->parseShapeItem($tokens, $type); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); + return $items; + } + + /** @phpstan-impure */ + private function parseArrayShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ArrayShapeNode + { + $items = $this->parseShape($tokens, $type); + return new Ast\Type\ArrayShapeNode($items); } + /** @phpstan-impure */ + private function parseObjectShape(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ObjectShapeNode + { + $items = $this->parseShape($tokens, $type); + + return new Ast\Type\ObjectShapeNode($type, $items); + } + /** @phpstan-impure */ - private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode + private function parseShapeItem(TokenIterator $tokens, Ast\Type\IdentifierTypeNode $type): Ast\Type\ShapeItemNode { try { $tokens->pushSavePoint(); - $key = $this->parseArrayShapeKey($tokens); + $key = $this->parseShapeKey($tokens); $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); $tokens->consumeTokenType(Lexer::TOKEN_COLON); $value = $this->parse($tokens); $tokens->dropSavePoint(); + if ($type->name === 'object') { + return new Ast\Type\ObjectShapeItemNode($key, $optional, $value); + } + return new Ast\Type\ArrayShapeItemNode($key, $optional, $value); } catch (ParserException $e) { $tokens->rollback(); $value = $this->parse($tokens); + if ($type->name === 'object') { + return new Ast\Type\ObjectShapeItemNode(null, false, $value); + } + return new Ast\Type\ArrayShapeItemNode(null, false, $value); } } @@ -523,7 +548,7 @@ private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShape * @phpstan-impure * @return Ast\ConstExpr\ConstExprIntegerNode|Ast\ConstExpr\ConstExprStringNode|Ast\Type\IdentifierTypeNode */ - private function parseArrayShapeKey(TokenIterator $tokens) + private function parseShapeKey(TokenIterator $tokens) { if ($tokens->isCurrentTokenType(Lexer::TOKEN_INTEGER)) { $key = new Ast\ConstExpr\ConstExprIntegerNode($tokens->currentTokenValue()); diff --git a/tests/PHPStan/Parser/PhpDocParserTest.php b/tests/PHPStan/Parser/PhpDocParserTest.php index ccf71651..015327f7 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -38,6 +38,8 @@ use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; @@ -880,13 +882,22 @@ public function provideVarTagsData(): Iterator new PhpDocNode([ new PhpDocTagNode( '@psalm-type', - new InvalidTagValueNode( - 'Unexpected token "{", expected \'*/\' at offset 44', - new ParserException( - '{', - Lexer::TOKEN_OPEN_CURLY_BRACKET, - 44, - Lexer::TOKEN_CLOSE_PHPDOC + new TypeAliasTagValueNode( + 'PARTSTRUCTURE_PARAM', + new ObjectShapeNode( + new IdentifierTypeNode('object'), + [ + new ObjectShapeItemNode( + new IdentifierTypeNode('attribute'), + false, + new IdentifierTypeNode('string') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('value'), + true, + new IdentifierTypeNode('string') + ), + ] ) ) ), diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 8289488b..9209b343 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -19,6 +19,8 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; use PHPStan\PhpDocParser\Ast\Type\OffsetAccessTypeNode; use PHPStan\PhpDocParser\Ast\Type\ThisTypeNode; use PHPStan\PhpDocParser\Ast\Type\TypeNode; @@ -1266,6 +1268,27 @@ public function provideParseData(): array ) ), ], + + [ + 'object{a: int, b?: ?int}', + new ObjectShapeNode( + new IdentifierTypeNode('object'), + [ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ObjectShapeItemNode( + new IdentifierTypeNode('b'), + true, + new NullableTypeNode( + new IdentifierTypeNode('int') + ) + ), + ] + ), + ], ]; }