diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php index 8d20cd11b57..0bbbfbe00f9 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php @@ -3,6 +3,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TypeCombiner; use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; @@ -11,6 +12,8 @@ use Psalm\Type\Union; use function array_reverse; +use function array_values; +use function count; /** * @internal @@ -57,17 +60,44 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($first_arg_array->is_list) { $second_arg = $call_args[1]->value ?? null; - if (!$second_arg - || (($second_arg_type = $statements_source->node_data->getType($second_arg)) - && $second_arg_type->isFalse() - ) - ) { - return $first_arg_array->fallback_params - ? ($first_arg_array->isNonEmpty() + $preserve_keys = + $second_arg && + !(($second_arg_type = $statements_source->node_data->getType($second_arg)) && + $second_arg_type->isFalse()); + if (!$preserve_keys) { + if ($first_arg_array->fallback_params) { + return $first_arg_array->isNonEmpty() ? Type::getNonEmptyList($first_arg_array->getGenericValueType()) - : Type::getList($first_arg_array->getGenericValueType()) - ) - : new Union([$first_arg_array->setProperties(array_reverse($first_arg_array->properties))]); + : Type::getList($first_arg_array->getGenericValueType()); + } + + $reversed_array_items = []; + $num_undefined = 0; + $i = 0; + foreach (array_reverse($first_arg_array->properties) as $array_item_type) { + $reversed_array_items[] = $array_item_type; + /** @var int<0,max> $j */ + $j = $i - $num_undefined; + for (; $j < $i; ++$j) { + $reversed_array_items[$j] = TypeCombiner::combine([ + ...array_values($reversed_array_items[$j]->getAtomicTypes()), + ...array_values($array_item_type->getAtomicTypes()), + ]); + } + if ($array_item_type->possibly_undefined) { + ++$num_undefined; + } + ++$i; + } + + $max_len = count($reversed_array_items); + /** @var int<0,max> $i */ + $i = $max_len - $num_undefined; + for (; $i < $max_len; ++$i) { + $reversed_array_items[$i] = $reversed_array_items[$i]->setPossiblyUndefined(true); + } + + return new Union([$first_arg_array->setProperties($reversed_array_items)]); } return new Union([new TKeyedArray( diff --git a/tests/ArrayFunctionCallTest.php b/tests/ArrayFunctionCallTest.php index 6abaf96ba68..a44a3cbfa5c 100644 --- a/tests/ArrayFunctionCallTest.php +++ b/tests/ArrayFunctionCallTest.php @@ -442,6 +442,31 @@ public function replace($a, $b): array '$d' => 'non-empty-array', ], ], + 'arrayReverseListDontPreserveKey' => [ + 'code' => ' [ + '$r' => 'list{0: bool|string, 1: bool|int|string, 2: false|float|int, 3: float|int, 4?: float|int, 5?: int}', + '$s' => 'list{0: int, 1: int, 2: int, 3?: int, 4?: int}', + ], + ], + 'arrayReverseListInt' => [ + 'code' => ' */ + function f(): array { return []; } + $a = array_reverse(f());', + 'assertions' => [ + '$a' => 'list', + ], + ], 'arrayReverseDontPreserveKeyExplicitArg' => [ 'code' => ' 4], false);',