From 845dcbf588c9c3efad5d2f93e2f1dcff01067dfd Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Wed, 20 Jul 2022 20:26:39 +0100 Subject: [PATCH 1/3] Added ability to parse object shapes. --- doc/grammars/type.abnf | 14 ++++++- src/Ast/Type/ArrayShapeItemNode.php | 43 +------------------- src/Ast/Type/ObjectShapeItemNode.php | 8 ++++ src/Ast/Type/ObjectShapeNode.php | 31 ++++++++++++++ src/Ast/Type/ShapeItemNode.php | 49 +++++++++++++++++++++++ src/Parser/TypeParser.php | 43 +++++++++++++++----- tests/PHPStan/Parser/PhpDocParserTest.php | 25 ++++++++---- tests/PHPStan/Parser/TypeParserTest.php | 23 +++++++++++ 8 files changed, 177 insertions(+), 59 deletions(-) create mode 100644 src/Ast/Type/ObjectShapeItemNode.php create mode 100644 src/Ast/Type/ObjectShapeNode.php create mode 100644 src/Ast/Type/ShapeItemNode.php diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 5af5be1f..91679408 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -60,12 +60,24 @@ Array = 1*(TokenSquareBracketOpen TokenSquareBracketClose) ArrayShape - = TokenCurlyBracketOpen ArrayShapeItem *(TokenComma ArrayShapeItem) TokenCurlyBracketClose + = Shape ArrayShapeItem + = 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') + ) + ), + ] + ), + ], ]; } From 8a2dc93d612ce7e2227856f7ac512a335ea762da Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Wed, 20 Jul 2022 20:55:53 +0100 Subject: [PATCH 2/3] Fixed tab usage instead of space in type.abnf file. Added editorconfig entry for .abnf. --- .editorconfig | 4 ++++ doc/grammars/type.abnf | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 5d66bc42..77e6b61b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -25,3 +25,7 @@ indent_size = 2 [composer.json] indent_style = tab indent_size = 4 + +[*.abnf] +indent_style = space +indent_size = 4 diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 91679408..505f9b9d 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -73,7 +73,7 @@ ShapeItem / Type ObjectShape - = Shape + = Shape ObjectShapeItem = ShapeItem From 8e50810b329cbae58f4b8ba5c4ec6112ad0300c8 Mon Sep 17 00:00:00 2001 From: Brad <28307684+mad-briller@users.noreply.github.com> Date: Wed, 20 Jul 2022 22:21:08 +0100 Subject: [PATCH 3/3] Fixed tabs vs spaces again. Set editorconfig to use tabs. --- .editorconfig | 2 +- doc/grammars/type.abnf | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 77e6b61b..07a06ef0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -27,5 +27,5 @@ indent_style = tab indent_size = 4 [*.abnf] -indent_style = space +indent_style = tab indent_size = 4 diff --git a/doc/grammars/type.abnf b/doc/grammars/type.abnf index 505f9b9d..571c225b 100644 --- a/doc/grammars/type.abnf +++ b/doc/grammars/type.abnf @@ -63,20 +63,20 @@ ArrayShape = Shape ArrayShapeItem - = ShapeItem + = ShapeItem Shape - = TokenCurlyBracketOpen ShapeItem *(TokenComma ShapeItem) TokenCurlyBracketClose + = TokenCurlyBracketOpen ShapeItem *(TokenComma ShapeItem) TokenCurlyBracketClose ShapeItem - = (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type - / Type + = (ConstantString / ConstantInt / TokenIdentifier) TokenNullable TokenColon Type + / Type ObjectShape - = Shape + = Shape ObjectShapeItem - = ShapeItem + = ShapeItem ; ---------------------------------------------------------------------------- ; ; ConstantExpr ;