From 6254ad1e487ed8e4099b594490ce23fd93e4e7ba Mon Sep 17 00:00:00 2001 From: Jeremiasz Major Date: Wed, 30 Nov 2022 19:39:18 +0100 Subject: [PATCH] Support unsealed array shapes --- src/Ast/Type/ArrayShapeNode.php | 14 ++++- src/Parser/TypeParser.php | 23 +++++---- tests/PHPStan/Parser/TypeParserTest.php | 69 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 38d64dd6..b0683351 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -13,15 +13,25 @@ class ArrayShapeNode implements TypeNode /** @var ArrayShapeItemNode[] */ public $items; - public function __construct(array $items) + /** @var bool */ + public $sealed; + + public function __construct(array $items, bool $sealed = true) { $this->items = $items; + $this->sealed = $sealed; } public function __toString(): string { - return 'array{' . implode(', ', $this->items) . '}'; + $items = $this->items; + + if ($this->sealed) { + $items[] = '...'; + } + + return 'array{' . implode(', ', $items) . '}'; } } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ef0fbc90..f45aafdc 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -503,29 +503,32 @@ private function tryParseArrayOrOffsetAccess(TokenIterator $tokens, Ast\Type\Typ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type): Ast\Type\ArrayShapeNode { $tokens->consumeTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET); - if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - return new Ast\Type\ArrayShapeNode([]); - } - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - $items = [$this->parseArrayShapeItem($tokens)]; + $items = []; + $sealed = true; - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET)) { - // trailing comma case return new Ast\Type\ArrayShapeNode($items); } + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { + $sealed = false; + $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); + break; + } + $items[] = $this->parseArrayShapeItem($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - } + } while ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items); + return new Ast\Type\ArrayShapeNode($items, $sealed); } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 75b26fe7..484823a8 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -599,6 +599,75 @@ public function provideParseData(): array ), ]), ], + [ + 'array{a: int, b: int, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{int, string, ...}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string') + ), + ], false), + ], + [ + 'array{...}', + new ArrayShapeNode([], false), + ], + [ + 'array{ + * a: int, + * ... + *}', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{ + a: int, + ..., + }', + new ArrayShapeNode([ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + ], false), + ], + [ + 'array{int, ..., string}', + new ParserException( + 'string', + Lexer::TOKEN_IDENTIFIER, + 16, + Lexer::TOKEN_CLOSE_CURLY_BRACKET + ), + ], [ 'callable(): Foo', new CallableTypeNode(