diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index bafc163298a..0b38890dfd6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -93,6 +93,28 @@ public static function analyze( ); } + // if this array looks like an object-like array, let's return that instead + if ($array_creation_info->can_create_objectlike + && $array_creation_info->property_types + ) { + $object_like = new TKeyedArray( + $array_creation_info->property_types, + $array_creation_info->class_strings + ); + $object_like->sealed = true; + $object_like->is_list = $array_creation_info->all_list; + + $stmt_type = new Union([$object_like]); + + if ($array_creation_info->parent_taint_nodes) { + $stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes; + } + + $statements_analyzer->node_data->setType($stmt, $stmt_type); + + return true; + } + if ($array_creation_info->item_key_atomic_types) { $item_key_type = TypeCombiner::combine( $array_creation_info->item_key_atomic_types, @@ -123,31 +145,6 @@ public static function analyze( return true; } - // if this array looks like an object-like array, let's return that instead - if ($item_value_type - && $item_key_type - && ($item_key_type->hasString() || $item_key_type->hasInt()) - && $array_creation_info->can_create_objectlike - && $array_creation_info->property_types - ) { - $object_like = new TKeyedArray( - $array_creation_info->property_types, - $array_creation_info->class_strings - ); - $object_like->sealed = true; - $object_like->is_list = $array_creation_info->all_list; - - $stmt_type = new Union([$object_like]); - - if ($array_creation_info->parent_taint_nodes) { - $stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes; - } - - $statements_analyzer->node_data->setType($stmt, $stmt_type); - - return true; - } - if ($array_creation_info->all_list) { if (empty($array_creation_info->item_key_atomic_types)) { $array_type = new TList($item_value_type ?? Type::getMixed()); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 894c78ba786..a1bd13926ee 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -8,8 +8,11 @@ use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; +use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; +use Psalm\Type\Atomic\TKeyedArray; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Union; use UnexpectedValueException; @@ -49,7 +52,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && ($array_arg_type = $nodeTypeProvider->getType($call_args[0]->value)) && $array_arg_type->isSingle() && $array_arg_type->hasArray() - && ($array_type = ArrayType::infer($array_arg_type->getAtomicTypes()['array'])) + && ($array_type = ArrayType::infer($array_arg_type->getSingleAtomic())) ) { return $array_type->value; } @@ -58,24 +61,43 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $min_bounds = []; $max_bounds = []; foreach ($call_args as $arg) { - if ($array_arg_type = $nodeTypeProvider->getType($arg->value)) { - foreach ($array_arg_type->getAtomicTypes() as $atomic_type) { - if (!$atomic_type instanceof TInt) { - $all_int = false; - break; - } - - if ($atomic_type instanceof TLiteralInt) { - $min_bounds[] = $atomic_type->value; - $max_bounds[] = $atomic_type->value; - } elseif ($atomic_type instanceof TIntRange) { - $min_bounds[] = $atomic_type->min_bound; - $max_bounds[] = $atomic_type->max_bound; - } elseif (get_class($atomic_type) === TInt::class) { - $min_bounds[] = null; - $max_bounds[] = null; + if ($arg_type = $nodeTypeProvider->getType($arg->value)) { + if ($arg->unpack) { + if (!$arg_type->isSingle() || !$arg_type->isArray()) { + return Type::getMixed(); } else { - throw new UnexpectedValueException('Unexpected type'); + $array_arg_type = $arg_type->getSingleAtomic(); + if ($array_arg_type instanceof TKeyedArray) { + $possibly_unpacked_arg_types = $array_arg_type->properties; + } elseif ($array_arg_type instanceof TList) { + $possibly_unpacked_arg_types = [$array_arg_type->type_param]; + } else { + assert($array_arg_type instanceof TArray); + $possibly_unpacked_arg_types = [$array_arg_type->type_params[1]]; + } + } + } else { + $possibly_unpacked_arg_types = [$arg_type]; + } + foreach ($possibly_unpacked_arg_types as $possibly_unpacked_arg_type) { + foreach ($possibly_unpacked_arg_type->getAtomicTypes() as $atomic_type) { + if (!$atomic_type instanceof TInt) { + $all_int = false; + break 2; + } + + if ($atomic_type instanceof TLiteralInt) { + $min_bounds[] = $atomic_type->value; + $max_bounds[] = $atomic_type->value; + } elseif ($atomic_type instanceof TIntRange) { + $min_bounds[] = $atomic_type->min_bound; + $max_bounds[] = $atomic_type->max_bound; + } elseif (get_class($atomic_type) === TInt::class) { + $min_bounds[] = null; + $max_bounds[] = null; + } else { + throw new UnexpectedValueException('Unexpected type'); + } } } } else { @@ -114,10 +136,21 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev //if we're dealing with non-int elements, just combine them all together $return_type = null; foreach ($call_args as $arg) { - if ($array_arg_type = $nodeTypeProvider->getType($arg->value)) { + if ($arg_type = $nodeTypeProvider->getType($arg->value)) { + if ($arg->unpack) { + if ($arg_type->isSingle() && $arg_type->isArray()) { + $array_type = ArrayType::infer($arg_type->getSingleAtomic()); + assert($array_type !== null); + $additional_type = $array_type->value; + } else { + $additional_type = Type::getMixed(); + } + } else { + $additional_type = $arg_type; + } $return_type = Type::combineUnionTypes( $return_type, - $array_arg_type + $additional_type, ); } else { return Type::getMixed(); diff --git a/src/Psalm/Internal/Type/ArrayType.php b/src/Psalm/Internal/Type/ArrayType.php index 00238803ed9..b524e2d33e8 100644 --- a/src/Psalm/Internal/Type/ArrayType.php +++ b/src/Psalm/Internal/Type/ArrayType.php @@ -32,6 +32,15 @@ public function __construct(Union $key, Union $value, bool $is_list) $this->is_list = $is_list; } + /** + * @return ( + * $type is TArrayKey ? self : ( + * $type is TArray ? self : ( + * $type is TList ? self : null + * ) + * ) + * ) + */ public static function infer(Atomic $type): ?self { if ($type instanceof TKeyedArray) { diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 1a7dd1c7332..2fcbd0784f2 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1669,6 +1669,36 @@ function foo(DateTimeImmutable $fooDate): string '$a' => 'DateTimeImmutable|float', ], ], + 'maxUnpackArray' => [ + 'code' => ' [ + 'code' => ' [ + '$max===' => '1|2|3|4|DateTime', + ], + ], 'strtolowerEmptiness' => [ 'code' => ' [ '$f===' => 'int', @@ -617,6 +619,8 @@ function getInt(): int{return 0;} '$i===' => 'int<20, max>', '$j===' => 'int<20, max>', '$k===' => 'int<40, max>', + '$l===' => 'int', + '$m===' => 'int<20, max>', ], ], 'dontCrashOnFalsy' => [ @@ -813,7 +817,23 @@ function matches($key, int $expected): bool { return true; }' - ] + ], + 'literalArrayUnpack' => [ + 'code' => ' */ + $a = 2; + /** @var int<6, 10> */ + $b = 9; + + /** + * @param int<0, 5> $_a + * @param int<6, 10> $_b + */ + function foo(int $_a, int $_b): void {} + + foo(...[$a, $b]); + ', + ], ]; }