diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 1b64ce3805f..f1890d3ded1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -650,17 +650,18 @@ private static function updateArrayAssignmentChildType( } else { assert($array_atomic_type_list !== null); $array_atomic_type = new TKeyedArray( - array_fill( + [...array_fill( + $atomic_root_type_array->getMinCount(), + count($atomic_root_type_array->properties)-1, + $array_atomic_type_list + ), ...array_fill( 0, - count($atomic_root_type_array->properties), - $array_atomic_type_list, - ), + count($atomic_root_type_array->properties)-1, + Type::getNever() + )], null, - [ - Type::getListKey(), - $array_atomic_type_list, - ], - true, + null, + true ); } $from_countable_object_like = true; diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index df48a09e3f6..11a1ccf4a89 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -85,6 +85,19 @@ public function __construct( $this->fallback_params = $fallback_params; $this->is_list = $is_list; $this->from_docblock = $from_docblock; + if ($this->is_list) { + $last_k = -1; + $had_possibly_undefined = false; + ksort($this->properties); + foreach ($this->properties as $k => $v) { + if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) { + $this->is_list = false; + break; + } + $had_possibly_undefined = $v->possibly_undefined || $had_possibly_undefined; + $last_k = $k; + } + } } /** @@ -98,6 +111,19 @@ public function setProperties(array $properties): self } $cloned = clone $this; $cloned->properties = $properties; + if ($cloned->is_list) { + $last_k = -1; + $had_possibly_undefined = false; + ksort($cloned->properties); + foreach ($cloned->properties as $k => $v) { + if (is_string($k) || $last_k !== ($k-1) || ($had_possibly_undefined && !$v->possibly_undefined)) { + $cloned->is_list = false; + break; + } + $had_possibly_undefined = $v->possibly_undefined || $had_possibly_undefined; + $last_k = $k; + } + } return $cloned; } diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 2ea42045234..7538885ef76 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -433,6 +433,44 @@ function foo(array $a, int $b): void { public function providerValidCodeParse(): iterable { return [ + 'testBuildList' => [ + 'code' => ' [ + '$pre===' => 'list{0?: 0|1, 1?: 1}', + '$a===' => 'list{0: 0|1|2, 1?: 1|2, 2?: 2}', + ] + ], + 'testBuildListOther' => [ + 'code' => ' [ + '$list===' => "list{0: 'A'|'B'|'C', 1?: 'C'}" + ] + ], 'instanceOfStringOffset' => [ 'code' => '