diff --git a/src/Ast/PhpDoc/TemplateTagValueNode.php b/src/Ast/PhpDoc/TemplateTagValueNode.php index 4c82ba74..5847df22 100644 --- a/src/Ast/PhpDoc/TemplateTagValueNode.php +++ b/src/Ast/PhpDoc/TemplateTagValueNode.php @@ -26,7 +26,7 @@ public function __construct(string $name, ?TypeNode $bound, string $description) public function __toString(): string { - $bound = $this->bound ? " of {$this->bound}" : ''; + $bound = $this->bound !== null ? " of {$this->bound}" : ''; return trim("{$this->name}{$bound} {$this->description}"); } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 5289c947..9279b5a5 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -221,12 +221,22 @@ private function tryParseArray(TokenIterator $tokens, Ast\Type\TypeNode $type): private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\TypeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $items = [$this->parseArrayShapeItem($tokens)]; + $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); + } + $items[] = $this->parseArrayShapeItem($tokens); + $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); diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index a9e5e8df..c7b6c9e4 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -465,6 +465,90 @@ public function provideParseData(): array Lexer::TOKEN_IDENTIFIER ), ], + [ + 'array{ + * a: int + *}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'array{ + a: int, + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ]), + ], + [ + 'array{ + a: int, + b: string, + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'array{ + a: int + , b: string + , c: string + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('c'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], + [ + 'array{ + a: int, + b: string + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('string') + ), + ]), + ], [ 'callable(): Foo', new CallableTypeNode(