From a034ac3e83a104fdf34adc5ffad4489da4d45f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Sun, 25 Aug 2024 03:42:43 +0200 Subject: [PATCH 1/6] Support typing extra items in unsealed array shapes --- src/Ast/Type/ArrayShapeNode.php | 23 +- src/Parser/TypeParser.php | 16 +- src/Printer/Printer.php | 7 +- tests/PHPStan/Parser/TypeParserTest.php | 417 ++++++++++++++++++++++++ 4 files changed, 459 insertions(+), 4 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index 806783f9..c57de56c 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -2,8 +2,11 @@ namespace PHPStan\PhpDocParser\Ast\Type; +use InvalidArgumentException; use PHPStan\PhpDocParser\Ast\NodeAttributes; use function implode; +use function strlen; +use function substr; class ArrayShapeNode implements TypeNode { @@ -22,15 +25,27 @@ class ArrayShapeNode implements TypeNode /** @var self::KIND_* */ public $kind; + /** @var GenericTypeNode|null */ + public $extraItemType; + /** * @param ArrayShapeItemNode[] $items * @param self::KIND_* $kind */ - public function __construct(array $items, bool $sealed = true, string $kind = self::KIND_ARRAY) + public function __construct( + array $items, + bool $sealed = true, + string $kind = self::KIND_ARRAY, + ?GenericTypeNode $extraItemType = null + ) { $this->items = $items; $this->sealed = $sealed; $this->kind = $kind; + $this->extraItemType = $extraItemType; + if ($sealed && $extraItemType !== null) { + throw new InvalidArgumentException('An extra item type may only be set for an unsealed array shape'); + } } @@ -39,7 +54,11 @@ public function __toString(): string $items = $this->items; if (! $this->sealed) { - $items[] = '...'; + $item = '...'; + if ($this->extraItemType !== null) { + $item .= substr((string) $this->extraItemType, strlen((string) $this->extraItemType->type)); + } + $items[] = $item; } return $this->kind . '{' . implode(', ', $items) . '}'; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 2e404655..68ff0a7e 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -848,6 +848,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items = []; $sealed = true; + $extraItemType = null; do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -858,6 +859,19 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { $sealed = false; + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + $extraItemType = $this->parseGeneric( + $tokens, + $this->enrichWithAttributes( + $tokens, + new Ast\Type\IdentifierTypeNode($kind), + $tokens->currentTokenLine(), + $tokens->currentTokenIndex() + ) + ); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); break; } @@ -870,7 +884,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items, $sealed, $kind); + return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $extraItemType); } diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 044d07f8..42edf2a5 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -74,6 +74,7 @@ use function sprintf; use function strlen; use function strpos; +use function substr; use function trim; use const PREG_SET_ORDER; @@ -366,7 +367,11 @@ private function printType(TypeNode $node): string }, $node->items); if (! $node->sealed) { - $items[] = '...'; + $item = '...'; + if ($node->extraItemType !== null) { + $item .= substr($this->printType($node->extraItemType), strlen((string) $node->extraItemType->type)); + } + $items[] = $item; } return $node->kind . '{' . implode(', ', $items) . '}'; diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index d6c66bb8..fb2b1a9a 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -760,6 +760,423 @@ public function provideParseData(): array ArrayShapeNode::KIND_LIST ), ], + [ + 'array{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a: int, b?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a:int,b?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a: int, b?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a: int, b?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a:int,b?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'array{a: int, b?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' int ' . PHP_EOL + . ' , ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new IdentifierTypeNode('a'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new IdentifierTypeNode('b'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_ARRAY, + new GenericTypeNode( + new IdentifierTypeNode('array'), + [ + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{...}', + new ArrayShapeNode( + [], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{int, int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{int,int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{int, int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + null, + false, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{0: int, 1?: int, ...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{0:int,1?:int,...}', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], + [ + 'list{0: int, 1?: int, ... ' . PHP_EOL + . ' < ' . PHP_EOL + . ' string ' . PHP_EOL + . ' > ' . PHP_EOL + . ' , ' . PHP_EOL + . ' }', + new ArrayShapeNode( + [ + new ArrayShapeItemNode( + new ConstExprIntegerNode('0'), + false, + new IdentifierTypeNode('int') + ), + new ArrayShapeItemNode( + new ConstExprIntegerNode('1'), + true, + new IdentifierTypeNode('int') + ), + ], + false, + ArrayShapeNode::KIND_LIST, + new GenericTypeNode( + new IdentifierTypeNode('list'), + [ + new IdentifierTypeNode('string'), + ], + [ + GenericTypeNode::VARIANCE_INVARIANT, + ] + ) + ), + ], [ 'callable(): Foo', new CallableTypeNode( From f374c5e9b7ed6a0792fdcef5e5d2832d66e1b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Sun, 25 Aug 2024 17:10:49 +0200 Subject: [PATCH 2/6] Use separate extra key/value types instead of a generic type --- src/Ast/Type/ArrayShapeNode.php | 28 +-- src/Parser/TypeParser.php | 34 ++-- src/Printer/Printer.php | 10 +- tests/PHPStan/Parser/TypeParserTest.php | 236 ++++++++++-------------- 4 files changed, 138 insertions(+), 170 deletions(-) diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index c57de56c..a37badd9 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -2,11 +2,8 @@ namespace PHPStan\PhpDocParser\Ast\Type; -use InvalidArgumentException; use PHPStan\PhpDocParser\Ast\NodeAttributes; use function implode; -use function strlen; -use function substr; class ArrayShapeNode implements TypeNode { @@ -25,8 +22,11 @@ class ArrayShapeNode implements TypeNode /** @var self::KIND_* */ public $kind; - /** @var GenericTypeNode|null */ - public $extraItemType; + /** @var TypeNode|null */ + public $extraKeyType; + + /** @var TypeNode|null */ + public $extraValueType; /** * @param ArrayShapeItemNode[] $items @@ -36,16 +36,15 @@ public function __construct( array $items, bool $sealed = true, string $kind = self::KIND_ARRAY, - ?GenericTypeNode $extraItemType = null + ?TypeNode $extraKeyType = null, + ?TypeNode $extraValueType = null ) { $this->items = $items; $this->sealed = $sealed; $this->kind = $kind; - $this->extraItemType = $extraItemType; - if ($sealed && $extraItemType !== null) { - throw new InvalidArgumentException('An extra item type may only be set for an unsealed array shape'); - } + $this->extraKeyType = $extraKeyType; + $this->extraValueType = $extraValueType; } @@ -55,8 +54,13 @@ public function __toString(): string if (! $this->sealed) { $item = '...'; - if ($this->extraItemType !== null) { - $item .= substr((string) $this->extraItemType, strlen((string) $this->extraItemType->type)); + if ($this->extraValueType !== null) { + $extraTypes = []; + if ($this->extraKeyType !== null) { + $extraTypes[] = (string) $this->extraKeyType; + } + $extraTypes[] = (string) $this->extraValueType; + $item .= '<' . implode(', ', $extraTypes) . '>'; } $items[] = $item; } diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 68ff0a7e..150e26f9 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -848,7 +848,8 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items = []; $sealed = true; - $extraItemType = null; + $extraKeyType = null; + $extraValueType = null; do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -859,19 +860,28 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, if ($tokens->tryConsumeTokenType(Lexer::TOKEN_VARIADIC)) { $sealed = false; + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { - $extraItemType = $this->parseGeneric( - $tokens, - $this->enrichWithAttributes( - $tokens, - new Ast\Type\IdentifierTypeNode($kind), - $tokens->currentTokenLine(), - $tokens->currentTokenIndex() - ) - ); + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $extraValueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $extraKeyType = $extraValueType; + $extraValueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } + $tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA); break; } @@ -884,7 +894,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $extraItemType); + return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $extraKeyType, $extraValueType); } diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index 42edf2a5..ae443438 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -74,7 +74,6 @@ use function sprintf; use function strlen; use function strpos; -use function substr; use function trim; use const PREG_SET_ORDER; @@ -368,8 +367,13 @@ private function printType(TypeNode $node): string if (! $node->sealed) { $item = '...'; - if ($node->extraItemType !== null) { - $item .= substr($this->printType($node->extraItemType), strlen((string) $node->extraItemType->type)); + if ($node->extraValueType !== null) { + $extraTypes = []; + if ($node->extraKeyType !== null) { + $extraTypes[] = $this->printType($node->extraKeyType); + } + $extraTypes[] = $this->printType($node->extraValueType); + $item .= '<' . implode(', ', $extraTypes) . '>'; } $items[] = $item; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index fb2b1a9a..066fdd68 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -766,15 +766,8 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -794,15 +787,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -822,15 +808,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -855,15 +834,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -872,17 +844,8 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string') ), ], [ @@ -902,17 +865,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string') ), ], [ @@ -932,17 +886,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string') ), ], [ @@ -969,17 +914,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new GenericTypeNode( - new IdentifierTypeNode('array'), - [ - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + new IdentifierTypeNode('int'), + new IdentifierTypeNode('string') ), ], [ @@ -988,15 +924,8 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1016,15 +945,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1044,15 +966,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1077,15 +992,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1105,15 +1013,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1133,15 +1034,8 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') ), ], [ @@ -1166,15 +1060,71 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - new GenericTypeNode( - new IdentifierTypeNode('list'), - [ - new IdentifierTypeNode('string'), - ], - [ - GenericTypeNode::VARIANCE_INVARIANT, - ] - ) + null, + new IdentifierTypeNode('string') + ), + ], + [ + 'array{...<>}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 10, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{...}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 14, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'array{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 21, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'array{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 21, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'list{...<>}', + new ParserException( + '>', + Lexer::TOKEN_CLOSE_ANGLE_BRACKET, + 9, + Lexer::TOKEN_IDENTIFIER + ), + ], + [ + 'list{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 12, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET + ), + ], + [ + 'list{...}', + new ParserException( + ',', + Lexer::TOKEN_COMMA, + 12, + Lexer::TOKEN_CLOSE_ANGLE_BRACKET ), ], [ From a2268bb4e9148df8ec5cee099a05aa0048c82f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Wed, 28 Aug 2024 03:51:30 +0200 Subject: [PATCH 3/6] Combine types into an ArrayShapeUnsealedTypeNode --- src/Ast/Type/ArrayShapeNode.php | 24 ++---- src/Ast/Type/ArrayShapeUnsealedTypeNode.php | 33 ++++++++ src/Parser/TypeParser.php | 81 ++++++++++++++---- src/Printer/Printer.php | 18 ++-- tests/PHPStan/Parser/TypeParserTest.php | 91 ++++++++++++++------- 5 files changed, 170 insertions(+), 77 deletions(-) create mode 100644 src/Ast/Type/ArrayShapeUnsealedTypeNode.php diff --git a/src/Ast/Type/ArrayShapeNode.php b/src/Ast/Type/ArrayShapeNode.php index a37badd9..1f4ed4a9 100644 --- a/src/Ast/Type/ArrayShapeNode.php +++ b/src/Ast/Type/ArrayShapeNode.php @@ -22,11 +22,8 @@ class ArrayShapeNode implements TypeNode /** @var self::KIND_* */ public $kind; - /** @var TypeNode|null */ - public $extraKeyType; - - /** @var TypeNode|null */ - public $extraValueType; + /** @var ArrayShapeUnsealedTypeNode|null */ + public $unsealedType; /** * @param ArrayShapeItemNode[] $items @@ -36,15 +33,13 @@ public function __construct( array $items, bool $sealed = true, string $kind = self::KIND_ARRAY, - ?TypeNode $extraKeyType = null, - ?TypeNode $extraValueType = null + ?ArrayShapeUnsealedTypeNode $unsealedType = null ) { $this->items = $items; $this->sealed = $sealed; $this->kind = $kind; - $this->extraKeyType = $extraKeyType; - $this->extraValueType = $extraValueType; + $this->unsealedType = $unsealedType; } @@ -53,16 +48,7 @@ public function __toString(): string $items = $this->items; if (! $this->sealed) { - $item = '...'; - if ($this->extraValueType !== null) { - $extraTypes = []; - if ($this->extraKeyType !== null) { - $extraTypes[] = (string) $this->extraKeyType; - } - $extraTypes[] = (string) $this->extraValueType; - $item .= '<' . implode(', ', $extraTypes) . '>'; - } - $items[] = $item; + $items[] = '...' . $this->unsealedType; } return $this->kind . '{' . implode(', ', $items) . '}'; diff --git a/src/Ast/Type/ArrayShapeUnsealedTypeNode.php b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php new file mode 100644 index 00000000..8c8b8e1a --- /dev/null +++ b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php @@ -0,0 +1,33 @@ +valueType = $valueType; + $this->keyType = $keyType; + } + + public function __toString(): string + { + if ($this->keyType !== null) { + return sprintf('<%s, %s>', $this->keyType, $this->valueType); + } + return sprintf('<%s>', $this->valueType); + } + +} diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index 150e26f9..c47ba10f 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -848,8 +848,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $items = []; $sealed = true; - $extraKeyType = null; - $extraValueType = null; + $unsealedType = null; do { $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); @@ -862,23 +861,12 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $sealed = false; $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - if ($tokens->tryConsumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - - $extraValueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - + if ($tokens->isCurrentTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET)) { if ($kind === Ast\Type\ArrayShapeNode::KIND_ARRAY) { - if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - - $extraKeyType = $extraValueType; - $extraValueType = $this->parse($tokens); - $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); - } + $unsealedType = $this->parseArrayShapeUnsealedType($tokens); + } else { + $unsealedType = $this->parseListShapeUnsealedType($tokens); } - - $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); } @@ -894,7 +882,7 @@ private function parseArrayShape(TokenIterator $tokens, Ast\Type\TypeNode $type, $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_CURLY_BRACKET); - return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $extraKeyType, $extraValueType); + return new Ast\Type\ArrayShapeNode($items, $sealed, $kind, $unsealedType); } @@ -973,6 +961,63 @@ private function parseArrayShapeKey(TokenIterator $tokens) ); } + /** + * @phpstan-impure + */ + private function parseArrayShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $keyType = null; + if ($tokens->tryConsumeTokenType(Lexer::TOKEN_COMMA)) { + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $keyType = $valueType; + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + } + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, $keyType), + $startLine, + $startIndex + ); + } + + /** + * @phpstan-impure + */ + private function parseListShapeUnsealedType(TokenIterator $tokens): Ast\Type\ArrayShapeUnsealedTypeNode + { + $startLine = $tokens->currentTokenLine(); + $startIndex = $tokens->currentTokenIndex(); + + $tokens->consumeTokenType(Lexer::TOKEN_OPEN_ANGLE_BRACKET); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $valueType = $this->parse($tokens); + $tokens->tryConsumeTokenType(Lexer::TOKEN_PHPDOC_EOL); + + $tokens->consumeTokenType(Lexer::TOKEN_CLOSE_ANGLE_BRACKET); + + return $this->enrichWithAttributes( + $tokens, + new Ast\Type\ArrayShapeUnsealedTypeNode($valueType, null), + $startLine, + $startIndex + ); + } + /** * @phpstan-impure */ diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index ae443438..fdbe9fe8 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -43,6 +43,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -366,16 +367,7 @@ private function printType(TypeNode $node): string }, $node->items); if (! $node->sealed) { - $item = '...'; - if ($node->extraValueType !== null) { - $extraTypes = []; - if ($node->extraKeyType !== null) { - $extraTypes[] = $this->printType($node->extraKeyType); - } - $extraTypes[] = $this->printType($node->extraValueType); - $item .= '<' . implode(', ', $extraTypes) . '>'; - } - $items[] = $item; + $items[] = '...' . ($node->unsealedType === null ? '' : $this->printType($node->unsealedType)); } return $node->kind . '{' . implode(', ', $items) . '}'; @@ -392,6 +384,12 @@ private function printType(TypeNode $node): string return $this->printType($node->valueType); } + if ($node instanceof ArrayShapeUnsealedTypeNode) { + if ($node->keyType !== null) { + return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType)); + } + return sprintf('<%s>', $this->printType($node->valueType)); + } if ($node instanceof ArrayTypeNode) { return $this->printOffsetAccessType($node->type) . '[]'; } diff --git a/tests/PHPStan/Parser/TypeParserTest.php b/tests/PHPStan/Parser/TypeParserTest.php index 066fdd68..3610fa52 100644 --- a/tests/PHPStan/Parser/TypeParserTest.php +++ b/tests/PHPStan/Parser/TypeParserTest.php @@ -13,6 +13,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -766,8 +767,10 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_ARRAY, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -787,8 +790,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -808,8 +813,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -834,8 +841,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -844,8 +853,10 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_ARRAY, - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) ), ], [ @@ -865,8 +876,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) ), ], [ @@ -886,8 +899,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) ), ], [ @@ -914,8 +929,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_ARRAY, - new IdentifierTypeNode('int'), - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + new IdentifierTypeNode('int') + ) ), ], [ @@ -924,8 +941,10 @@ public function provideParseData(): array [], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -945,8 +964,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -966,8 +987,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -992,8 +1015,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -1013,8 +1038,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -1034,8 +1061,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ @@ -1060,8 +1089,10 @@ public function provideParseData(): array ], false, ArrayShapeNode::KIND_LIST, - null, - new IdentifierTypeNode('string') + new ArrayShapeUnsealedTypeNode( + new IdentifierTypeNode('string'), + null + ) ), ], [ From 8a0bb9460d0422b1c98eae0197fc54a6c5b35099 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 28 Aug 2024 21:24:43 +0200 Subject: [PATCH 4/6] More tests --- tests/PHPStan/Printer/PrinterTest.php | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 07d68af0..9fbb8adf 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -28,6 +28,7 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\TypeAliasImportTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; +use PHPStan\PhpDocParser\Ast\Type\ArrayShapeUnsealedTypeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeParameterNode; @@ -51,6 +52,7 @@ use function array_splice; use function array_unshift; use function array_values; +use function assert; use function count; use const PHP_EOL; @@ -1740,6 +1742,76 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->unsealedType = new ArrayShapeUnsealedTypeNode(new IdentifierTypeNode('string'), null); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + assert($node->unsealedType !== null); + $node->unsealedType->keyType = new IdentifierTypeNode('int'); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return array{foo: int, ...} */', + '/** @return array{foo: int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + $node->unsealedType = null; + } + + return $node; + } + + }, + ]; } /** From 35b60c2464b93cd3fda29146756e4c61c4e426ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:34:58 +0200 Subject: [PATCH 5/6] Make ArrayShapeUnsealedTypeNode just a Node --- src/Ast/Type/ArrayShapeUnsealedTypeNode.php | 3 ++- src/Printer/Printer.php | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Ast/Type/ArrayShapeUnsealedTypeNode.php b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php index 8c8b8e1a..7ffdf1d2 100644 --- a/src/Ast/Type/ArrayShapeUnsealedTypeNode.php +++ b/src/Ast/Type/ArrayShapeUnsealedTypeNode.php @@ -2,10 +2,11 @@ namespace PHPStan\PhpDocParser\Ast\Type; +use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeAttributes; use function sprintf; -class ArrayShapeUnsealedTypeNode implements TypeNode +class ArrayShapeUnsealedTypeNode implements Node { use NodeAttributes; diff --git a/src/Printer/Printer.php b/src/Printer/Printer.php index fdbe9fe8..c4b9c356 100644 --- a/src/Printer/Printer.php +++ b/src/Printer/Printer.php @@ -230,6 +230,12 @@ function (PhpDocChildNode $child): string { $isOptional = $node->isOptional ? '=' : ''; return trim("{$type}{$isReference}{$isVariadic}{$node->parameterName}") . $isOptional; } + if ($node instanceof ArrayShapeUnsealedTypeNode) { + if ($node->keyType !== null) { + return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType)); + } + return sprintf('<%s>', $this->printType($node->valueType)); + } if ($node instanceof DoctrineAnnotation) { return (string) $node; } @@ -367,7 +373,7 @@ private function printType(TypeNode $node): string }, $node->items); if (! $node->sealed) { - $items[] = '...' . ($node->unsealedType === null ? '' : $this->printType($node->unsealedType)); + $items[] = '...' . ($node->unsealedType === null ? '' : $this->print($node->unsealedType)); } return $node->kind . '{' . implode(', ', $items) . '}'; @@ -384,12 +390,6 @@ private function printType(TypeNode $node): string return $this->printType($node->valueType); } - if ($node instanceof ArrayShapeUnsealedTypeNode) { - if ($node->keyType !== null) { - return sprintf('<%s, %s>', $this->printType($node->keyType), $this->printType($node->valueType)); - } - return sprintf('<%s>', $this->printType($node->valueType)); - } if ($node instanceof ArrayTypeNode) { return $this->printOffsetAccessType($node->type) . '[]'; } From dde139f599d3c7b4b1ee5e2d152404760b8fc64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tinjo=20Sch=C3=B6ni?= <32767367+tscni@users.noreply.github.com> Date: Wed, 28 Aug 2024 23:43:47 +0200 Subject: [PATCH 6/6] Add more PrinterTest cases for list shapes --- tests/PHPStan/Printer/PrinterTest.php | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/PHPStan/Printer/PrinterTest.php b/tests/PHPStan/Printer/PrinterTest.php index 9fbb8adf..24a28236 100644 --- a/tests/PHPStan/Printer/PrinterTest.php +++ b/tests/PHPStan/Printer/PrinterTest.php @@ -1812,6 +1812,58 @@ public function enterNode(Node $node) }, ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int, ...} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->unsealedType = new ArrayShapeUnsealedTypeNode(new IdentifierTypeNode('string'), null); + } + + return $node; + } + + }, + ]; + + yield [ + '/** @return list{int, ...} */', + '/** @return list{int} */', + new class extends AbstractNodeVisitor { + + public function enterNode(Node $node) + { + if ($node instanceof ArrayShapeNode) { + $node->sealed = true; + $node->unsealedType = null; + } + + return $node; + } + + }, + ]; } /**