diff --git a/src/Ast/Type/ObjectShapeItemNode.php b/src/Ast/Type/ObjectShapeItemNode.php new file mode 100644 index 00000000..406d6360 --- /dev/null +++ b/src/Ast/Type/ObjectShapeItemNode.php @@ -0,0 +1,7 @@ +items = $items; + } + + + public function __toString(): string + { + return 'object{' . implode(', ', $this->items) . '}'; + } + +} diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 170b2581..08854269 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -123,8 +123,8 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); - } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type); + } elseif (in_array($type->name, ['array', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + $type = $this->parseArrayOrObjectShape($tokens, $type); if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); @@ -409,8 +409,8 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { $type = $this->parseGeneric($tokens, $type); - } elseif ($type->name === 'array' && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { - $type = $this->parseArrayShape($tokens, $type); + } elseif (in_array($type->name, ['array', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + $type = $this->parseArrayOrObjectShape($tokens, $type); } } @@ -470,52 +470,52 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ /** @phpstan-impure */ - private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode + private function parseArrayOrObjectShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode|Ast\Type\ObjectShapeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - return new Ast\Type\ArrayShapeNode([]); + return $type->name === 'array' ? new Ast\Type\ArrayShapeNode([]) : new Ast\Type\ObjectShapeNode([]); } $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $items = [$this->parseArrayShapeItem($tokens)]; + $items = [$this->parseArrayOrObjectShapeItem($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 $type->name === 'array' ? new Ast\Type\ArrayShapeNode($items) : new Ast\Type\ObjectShapeNode($items); } - $items[] = $this->parseArrayShapeItem($tokens); + $items[] = $this->parseArrayOrObjectShapeItem($tokens, $type); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items); + return $type->name === 'array' ? new Ast\Type\ArrayShapeNode($items): new Ast\Type\ObjectShapeNode($items); } /** @phpstan-impure */ - private function parseArrayShapeItem(TokenIterator $tokens): Ast\Type\ArrayShapeItemNode + private function parseArrayOrObjectShapeItem(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeItemNode|Ast\Type\ObjectShapeItemNode { try { $tokens->pushSavePoint(); - $key = $this->parseArrayShapeKey($tokens); + $key = $this->parseArrayOrObjectShapeKey($tokens); $optional = $tokens->tryConsumeTokenType(Lexer::TOKEN_NULLABLE); $tokens->consumeTokenType(Lexer::TOKEN_COLON); $value = $this->parse($tokens); $tokens->dropSavePoint(); - return new Ast\Type\ArrayShapeItemNode($key, $optional, $value); + return $type->name === 'array' ? new Ast\Type\ArrayShapeItemNode($key, $optional, $value) : new Ast\Type\ObjectShapeItemNode($key, $optional, $value); } catch (ParserException $e) { $tokens->rollback(); $value = $this->parse($tokens); - return new Ast\Type\ArrayShapeItemNode(null, false, $value); + return $type->name === 'array' ? new Ast\Type\ArrayShapeItemNode(null, false, $value) : new Ast\Type\ObjectShapeItemNode(null, false, $value); } } @@ -523,7 +523,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 parseArrayOrObjectShapeKey(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 fa9c06fb..c6b3d673 100644 --- a/tests/PHPStan/Parser/PhpDocParserTest.php +++ b/tests/PHPStan/Parser/PhpDocParserTest.php @@ -44,6 +44,8 @@ use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPUnit\Framework\TestCase; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ObjectShapeItemNode; use const PHP_EOL; class PhpDocParserTest extends TestCase @@ -894,6 +896,69 @@ public function provideVarTagsData(): Iterator ), ]), ]; + + yield [ + 'OK without variable and description object shape', + '/** @var object{a: int} */', + new PhpDocNode([ + new PhpDocTagNode( + '@var', + new VarTagValueNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ) + ]), + '', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with variable object shape', + '/** @var object{a: int} $foo */', + new PhpDocNode([ + new PhpDocTagNode( + '@var', + new VarTagValueNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ) + ]), + '$foo', + '' + ) + ), + ]), + ]; + + yield [ + 'OK with variable and description object shape', + '/** @var object{a: int} $foo some description */', + new PhpDocNode([ + new PhpDocTagNode( + '@var', + new VarTagValueNode( + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ) + ]), + '$foo', + 'some description' + ) + ), + ]), + ]; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 8289488b..1a479f22 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; @@ -592,6 +594,18 @@ public function provideParseData(): array ), ]), ], + [ + 'object{ + a: int + }', + new ObjectShapeNode([ + new ObjectShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ) + ]), + ], [ 'callable(): Foo', new CallableTypeNode(