diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 3034c10..1d9cf85 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -10,6 +10,8 @@ class ArrayShapeNode implements TypeNode public const KIND_ARRAY = 'array'; public const KIND_LIST = 'list'; + public const KIND_NON_EMPTY_ARRAY = 'non-empty-array'; + public const KIND_NON_EMPTY_LIST = 'non-empty-list'; use NodeAttributes; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index ae3fe2d..84a3880 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -166,7 +166,13 @@ private function parseAtomic(TokenIterator $tokens): Ast\Type\TypeNode } elseif ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_SQUARE_BRACKET)) { $type = $this->tryParseArrayOrOffsetAccess($tokens, $type); - } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + } elseif (in_array($type->name, [ + Ast\Type\ArrayShapeNode::KIND_ARRAY, + Ast\Type\ArrayShapeNode::KIND_LIST, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, + 'object', + ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { if ($type->name === 'object') { $type = $this->parseObjectShape($tokens); } else { @@ -665,7 +671,13 @@ private function parseCallableReturnType(TokenIterator $tokens): Ast\Type\TypeNo $startIndex, )); - } elseif (in_array($type->name, ['array', 'list', 'object'], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { + } elseif (in_array($type->name, [ + Ast\Type\ArrayShapeNode::KIND_ARRAY, + Ast\Type\ArrayShapeNode::KIND_LIST, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + Ast\Type\ArrayShapeNode::KIND_NON_EMPTY_LIST, + 'object', + ], true) && $tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_CURLY_BRACKET) && !$tokens->isPrecededByHorizontalWhitespace()) { if ($type->name === 'object') { $type = $this->parseObjectShape($tokens); } else { diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index ad19f10..6306cfb 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -767,6 +767,84 @@ public function provideParseData(): array ArrayShapeNode::KIND_LIST, ), ], + [ + 'non-empty-array{ + int, + string + }', + ArrayShapeNode::createSealed( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int'), + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string'), + ), + ], + ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + ), + ], + [ + 'callable(): non-empty-array{int, string}', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], ArrayShapeNode::createSealed( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int'), + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string'), + ), + ], + ArrayShapeNode::KIND_NON_EMPTY_ARRAY, + ), []), + ], + [ + 'callable(): non-empty-list{int, string}', + new CallableTypeNode(new IdentifierTypeNode('callable'), [], ArrayShapeNode::createSealed( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int'), + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string'), + ), + ], + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ), []), + ], + [ + 'non-empty-list{ + int, + string + }', + ArrayShapeNode::createSealed( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int'), + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('string'), + ), + ], + ArrayShapeNode::KIND_NON_EMPTY_LIST, + ), + ], [ 'array{...}', ArrayShapeNode::createUnsealed(