diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f154dc936..70c788cbd 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + tags['variablesfrom'][0]]]> @@ -439,11 +439,6 @@ traverse - - - self::$listKey - - classExtendsOrImplements diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 34502e118..9fdd3fcc4 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -64,11 +64,14 @@ class ParseTreeCreator $type_token = $this->type_tokens[$this->t]; switch ($type_token[0]) { - case '<': case '{': case ']': throw new TypeParseTreeException('Unexpected token ' . $type_token[0]); + case '<': + $this->handleLessThan(); + break; + case '[': $this->handleOpenSquareBracket(); break; @@ -232,6 +235,29 @@ class ParseTreeCreator $this->current_leaf = $new_parent_leaf; } + private function handleLessThan(): void + { + if (!$this->current_leaf instanceof FieldEllipsis) { + throw new TypeParseTreeException('Unexpected token <'); + } + + $current_parent = $this->current_leaf->parent; + + if (!$current_parent instanceof KeyedArrayTree) { + throw new TypeParseTreeException('Unexpected token <'); + } + + array_pop($current_parent->children); + + $generic_leaf = new GenericTree( + '', + $current_parent, + ); + $current_parent->children []= $generic_leaf; + + $this->current_leaf = $generic_leaf; + } + private function handleOpenSquareBracket(): void { if ($this->current_leaf instanceof Root) { diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index eabc6f61f..35ca41aa2 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -688,6 +688,9 @@ class TypeParser } if ($generic_type_value === 'list') { + if (count($generic_params) > 1) { + throw new TypeParseTreeException('Too many template parameters for list'); + } return Type::getListAtomic($generic_params[0], $from_docblock); } @@ -1352,6 +1355,16 @@ class TypeParser $sealed = true; + $extra_params = null; + + $last_property_branch = end($parse_tree->children); + if ($last_property_branch instanceof GenericTree + && $last_property_branch->value === '' + ) { + $extra_params = $last_property_branch->children; + array_pop($parse_tree->children); + } + foreach ($parse_tree->children as $i => $property_branch) { $class_string = false; @@ -1472,12 +1485,37 @@ class TypeParser return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock); } + if ($extra_params) { + if ($is_list && count($extra_params) !== 1) { + throw new TypeParseTreeException('Must have exactly one extra field!'); + } + if (!$is_list && count($extra_params) !== 2) { + throw new TypeParseTreeException('Must have exactly two extra fields!'); + } + $final_extra_params = $is_list ? [Type::getListKey(true)] : []; + foreach ($extra_params as $child_tree) { + $child_type = self::getTypeFromTree( + $child_tree, + $codebase, + null, + $template_type_map, + $type_aliases, + $from_docblock, + ); + if ($child_type instanceof Atomic) { + $child_type = new Union([$child_type]); + } + $final_extra_params []= $child_type; + } + $extra_params = $final_extra_params; + } return new $class( $properties, $class_strings, - $sealed + $extra_params ?? ($sealed ? null - : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()], + : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()] + ), $is_list, $from_docblock, ); @@ -1642,11 +1680,11 @@ class TypeParser $all_sealed = true; foreach ($intersection_types as $intersection_type) { - foreach ($intersection_type->properties as $property => $property_type) { - if ($intersection_type->fallback_params !== null) { - $all_sealed = false; - } + if ($intersection_type->fallback_params !== null) { + $all_sealed = false; + } + foreach ($intersection_type->properties as $property => $property_type) { if (!array_key_exists($property, $properties)) { $properties[$property] = $property_type; continue; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 314e3e129..3b98f05c9 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -484,12 +484,17 @@ abstract class Type } private static ?Union $listKey = null; + private static ?Union $listKeyFromDocblock = null; /** * @psalm-pure + * @psalm-suppress ImpureStaticProperty Used for caching */ - public static function getListKey(): Union + public static function getListKey(bool $from_docblock = false): Union { + if ($from_docblock) { + return self::$listKeyFromDocblock ??= new Union([new TIntRange(0, null, true)]); + } return self::$listKey ??= new Union([new TIntRange(0, null)]); } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index c5476a61b..8510b83dd 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -202,8 +202,11 @@ class TKeyedArray extends Atomic } $params_part = $this->fallback_params !== null - ? ', ...<' . $this->fallback_params[0]->getId($exact) . ', ' - . $this->fallback_params[1]->getId($exact) . '>' + ? ', ...<' . ($this->is_list + ? $this->fallback_params[1]->getId($exact) + : $this->fallback_params[0]->getId($exact) . ', ' + . $this->fallback_params[1]->getId($exact) + ) . '>' : ''; return $key . '{' . implode(', ', $property_strings) . $params_part . '}'; diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index c96f2c9f0..f18fee7eb 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1540,7 +1540,7 @@ class ArrayAssignmentTest extends TestCase $x = [...$x, ...$y]; ', - 'assertions' => ['$x===' => 'list{int, int, ..., int>}'], + 'assertions' => ['$x===' => 'list{int, int, ...}'], ], 'unpackEmptyKeepsCorrectKeys' => [ 'code' => ' [ // todo: this first type is not entirely correct //'$c===' => "list{int|string, ..., int|string>}", - '$c===' => "list{string, ..., int|string>}", - '$d===' => "list{string, ..., int|string>}", + '$c===' => "list{string, ...}", + '$d===' => "list{string, ...}", ], ], 'arrayMergeEmpty' => [ diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 934c1affe..6429cf6a6 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -624,7 +624,7 @@ class FunctionCallTest extends TestCase */ $elements = explode(" ", $string, $limit);', 'assertions' => [ - '$elements' => 'list{0?: string, 1?: string, 2?: string, ..., string>}', + '$elements' => 'list{0?: string, 1?: string, 2?: string, ...}', ], ], 'explodeWithDynamicDelimiter' => [ @@ -680,7 +680,7 @@ class FunctionCallTest extends TestCase */ $elements = explode($delim, $string, $limit);', 'assertions' => [ - '$elements' => 'list{0?: string, 1?: string, 2?: string, ..., string>}', + '$elements' => 'list{0?: string, 1?: string, 2?: string, ...}', ], ], 'explodeWithDynamicNonEmptyDelimiter' => [ diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 43f35b609..fe9022c7a 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -29,7 +29,7 @@ class ReturnTypeTest extends TestCase $result = ret(); ', 'assertions' => [ - '$result===' => 'list{0?: 0|a, 1?: 0|a, ..., a>}', + '$result===' => 'list{0?: 0|a, 1?: 0|a, ...}', ], ], 'arrayCombineInv' => [ @@ -46,7 +46,7 @@ class ReturnTypeTest extends TestCase $result = ret(); ', 'assertions' => [ - '$result===' => 'list{0?: 0|a, 1?: 0|a, ..., a>}', + '$result===' => 'list{0?: 0|a, 1?: 0|a, ...}', ], ], 'arrayCombine2' => [ diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index f116e3703..2b85c5517 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -105,7 +105,7 @@ class TypeCombinationTest extends TestCase ], ], 'complexArrayFallback2' => [ - 'list{0?: 0|a, 1?: 0|a, ..., a>}', + 'list{0?: 0|a, 1?: 0|a, ...}', [ 'list', 'list{0, 0}', @@ -639,7 +639,7 @@ class TypeCombinationTest extends TestCase ], ], 'combineNonEmptyListWithTKeyedArrayList' => [ - 'list{null|string, ..., string>}', + 'list{null|string, ...}', [ 'non-empty-list', 'array{null}', diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 0f7796267..b65e63d24 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -178,6 +178,21 @@ class TypeParseTest extends TestCase ); } + public function testUnsealedArray(): void + { + $this->assertSame('array{a: int, ...}', Type::parseString('array{a: int, ...}')->getId()); + } + + public function testUnsealedList(): void + { + $this->assertSame('list{int, ...}', Type::parseString('list{int, ...}')->getId()); + } + + public function testUnsealedListComplex(): void + { + $this->assertSame('list{array{a: 123}, ...<123>}', Type::parseString('list{0: array{a: 123}, ...<123>}')->getId()); + } + public function testIntersectionAfterGeneric(): void { $this->assertSame('Countable&iterable&I', (string) Type::parseString('Countable&iterable&I'));