From f0153721a458a42972676483c6dc8da46646f1ee Mon Sep 17 00:00:00 2001 From: adrew Date: Sat, 15 Apr 2023 18:46:22 +0300 Subject: [PATCH] Fix list template replacement --- psalm-baseline.xml | 1 - src/Psalm/Type/Atomic/TKeyedArray.php | 48 ++++++++++++++++++++++++--- tests/FunctionCallTest.php | 29 ++++++++++++++++ 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e53cd8c1c17..965b3127e18 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -549,7 +549,6 @@ properties[0]]]> properties[0]]]> - properties[0]]]> getList diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index f2814a8a115..966cfbb1116 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -149,15 +149,24 @@ public function isSealed(): bool return $this->fallback_params === null; } + /** + * @psalm-assert-if-true list{Union} $this->properties + * @psalm-assert-if-true list{Union, Union} $this->fallback_params + */ + public function isGenericList(): bool + { + return $this->is_list + && count($this->properties) === 1 + && $this->fallback_params + && $this->properties[0]->equals($this->fallback_params[1], true, true, false); + } + public function getId(bool $exact = true, bool $nested = false): string { $property_strings = []; if ($this->is_list) { - if (count($this->properties) === 1 - && $this->fallback_params - && $this->properties[0]->equals($this->fallback_params[1], true, true, false) - ) { + if ($this->isGenericList()) { $t = $this->properties[0]->possibly_undefined ? 'list' : 'non-empty-list'; return "$t<".$this->fallback_params[1]->getId($exact).'>'; } @@ -415,7 +424,7 @@ public function getGenericArrayType(bool $allow_non_empty = true, ?string $list_ public function isNonEmpty(): bool { - if ($this->is_list) { + if ($this->isGenericList()) { return !$this->properties[0]->possibly_undefined; } foreach ($this->properties as $property) { @@ -503,6 +512,35 @@ public function replaceTemplateTypesWithStandins( bool $add_lower_bound = false, int $depth = 0 ): self { + if ($input_type instanceof TKeyedArray + && $input_type->is_list + && $input_type->isSealed() + && $this->isGenericList() + ) { + $replaced_list_type = $this + ->getGenericArrayType() + ->replaceTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type->getGenericArrayType(), + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth, + ) + ->type_params[1] + ->setPossiblyUndefined(!$this->isNonEmpty()); + + $cloned = clone $this; + $cloned->properties = [$replaced_list_type]; + $cloned->fallback_params = [$this->fallback_params[1], $replaced_list_type]; + + return $cloned; + } + $properties = $this->properties; foreach ($properties as $offset => $property) { diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index d6c4f86b232..7905ac3378b 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -17,6 +17,35 @@ class FunctionCallTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'inferGenericListFromTuple' => [ + 'code' => ' $list + * @return list + */ + function testList(array $list): array { return $list; } + /** + * @template A + * @param non-empty-list $list + * @return non-empty-list + */ + function testNonEmptyList(array $list): array { return $list; } + /** + * @template A of list + * @param A $list + * @return A + */ + function testGenericList(array $list): array { return $list; } + $list = testList([1, 2, 3]); + $nonEmptyList = testNonEmptyList([1, 2, 3]); + $genericList = testGenericList([1, 2, 3]);', + 'assertions' => [ + '$list===' => 'list<1|2|3>', + '$nonEmptyList===' => 'non-empty-list<1|2|3>', + '$genericList===' => 'list{1, 2, 3}', + ], + ], 'countShapedArrays' => [ 'code' => '