From d8b85f1c04ee982e678685a1a55d57f1c62a519c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 21 Apr 2023 14:04:47 +0200 Subject: [PATCH] Implement unsealed array generic syntax --- .../Internal/Type/ParseTree/GenericTree.php | 5 +- src/Psalm/Internal/Type/ParseTreeCreator.php | 61 ++++++++++++++++++- tests/TypeParseTest.php | 5 ++ 3 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTree/GenericTree.php b/src/Psalm/Internal/Type/ParseTree/GenericTree.php index cefe8e4bb..29a2c7704 100644 --- a/src/Psalm/Internal/Type/ParseTree/GenericTree.php +++ b/src/Psalm/Internal/Type/ParseTree/GenericTree.php @@ -13,9 +13,12 @@ class GenericTree extends ParseTree public bool $terminated = false; - public function __construct(string $value, ?ParseTree $parent = null) + public bool $is_unsealed_array_shape; + + public function __construct(string $value, ?ParseTree $parent = null, bool $is_unsealed_array_shape = false) { $this->value = $value; $this->parent = $parent; + $this->is_unsealed_array_shape = $is_unsealed_array_shape; } } diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 34502e118..03137f13f 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; @@ -95,6 +98,11 @@ class ParseTreeCreator break; case '}': + if ($this->current_leaf instanceof GenericTree + && $this->current_leaf->is_unsealed_array_shape + ) { + break; + } do { if ($this->current_leaf->parent === null) { throw new TypeParseTreeException('Cannot parse array type'); @@ -232,6 +240,57 @@ 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 <'); + } + + // Pop FieldEllipsis + array_pop($current_parent->children); + + // Set the parent to the keyed array tree + $this->current_leaf = $current_parent; + $current_parent = $this->current_leaf->parent; + + // Avoid array{a: int, ...}&array + if ($current_parent instanceof IntersectionTree) { + throw new TypeParseTreeException("Can't intersect an unsealed array with another array!"); + } + + // Otherwise replace the array tree with an intersection tree + $new_intersection_parent = new IntersectionTree($current_parent); + + $this->current_leaf->parent = $new_intersection_parent; + + // Append old keyed array tree and new generic array tree to intersection tree + $new_leaf = new GenericTree( + 'array', + $new_intersection_parent, + true, + ); + + $new_intersection_parent->children = [ + $this->current_leaf, + $new_leaf, + ]; + + if ($current_parent) { + array_pop($current_parent->children); + $current_parent->children []= $new_intersection_parent; + } else { + $this->parse_tree = $new_intersection_parent; + } + + $this->current_leaf = $new_leaf; + } + private function handleOpenSquareBracket(): void { if ($this->current_leaf instanceof Root) { diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 0f7796267..6e0021f6f 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -178,6 +178,11 @@ class TypeParseTest extends TestCase ); } + public function testUnsealedArray(): void + { + $this->assertSame('array{a: int, ...}', Type::parseString('array{a: int, ...}')->getId()); + } + public function testIntersectionAfterGeneric(): void { $this->assertSame('Countable&iterable&I', (string) Type::parseString('Countable&iterable&I'));