diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 05b326f9b..5735438ba 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -143,7 +143,7 @@ class Context /** * A list of clauses in Conjunctive Normal Form * - * @var array + * @var list */ public $clauses = []; @@ -488,22 +488,24 @@ class Context */ public function removeReconciledClauses(array $changed_var_ids) { - $this->clauses = array_filter( - $this->clauses, - /** @return bool */ - function (Clause $c) use ($changed_var_ids) { - if ($c->wedge) { + $this->clauses = \array_values( + array_filter( + $this->clauses, + /** @return bool */ + function (Clause $c) use ($changed_var_ids) { + if ($c->wedge) { + return true; + } + + foreach ($c->possibilities as $key => $_) { + if (in_array($key, $changed_var_ids, true)) { + return false; + } + } + return true; } - - foreach ($c->possibilities as $key => $_) { - if (in_array($key, $changed_var_ids, true)) { - return false; - } - } - - return true; - } + ) ); } @@ -513,7 +515,7 @@ class Context * @param Union|null $new_type * @param StatementsAnalyzer|null $statements_analyzer * - * @return array + * @return list */ public static function filterClauses( $remove_var_id, diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 07b70d58a..dcc4194bd 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -17,12 +17,12 @@ class ReturnTypeCollector * Gets the return types from a list of statements * * @param array $stmts - * @param array $yield_types + * @param list $yield_types * @param bool $ignore_nullable_issues * @param bool $ignore_falsable_issues * @param bool $collapse_types * - * @return array a list of return types + * @return list a list of return types */ public static function getReturnTypes( array $stmts, @@ -272,6 +272,8 @@ class ReturnTypeCollector ] ), ]; + } else { + $yield_types = $yield_types; } } @@ -281,7 +283,7 @@ class ReturnTypeCollector /** * @param PhpParser\Node\Expr $stmt * - * @return array + * @return list */ protected static function getYieldTypeFromExpression(PhpParser\Node\Expr $stmt) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 795ae4d88..e96d92485 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -437,12 +437,22 @@ class ForeachAnalyzer if ($iterator_atomic_type instanceof Type\Atomic\TArray || $iterator_atomic_type instanceof Type\Atomic\ObjectLike + || $iterator_atomic_type instanceof Type\Atomic\TList ) { if ($iterator_atomic_type instanceof Type\Atomic\ObjectLike) { if (!$iterator_atomic_type->sealed) { $always_non_empty_array = false; } $iterator_atomic_type = $iterator_atomic_type->getGenericArrayType(); + } elseif ($iterator_atomic_type instanceof Type\Atomic\TList) { + if (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyList) { + $always_non_empty_array = false; + } + + $iterator_atomic_type = new Type\Atomic\TArray([ + Type::getInt(), + $iterator_atomic_type->type_param + ]); } elseif (!$iterator_atomic_type instanceof Type\Atomic\TNonEmptyArray) { $always_non_empty_array = false; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 3c06fb7fd..2b594106e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -55,6 +55,8 @@ class ArrayAnalyzer $codebase = $statements_analyzer->getCodebase(); + $all_list = true; + foreach ($stmt->items as $int_offset => $item) { if ($item === null) { continue; @@ -63,6 +65,8 @@ class ArrayAnalyzer $item_key_value = null; if ($item->key) { + $all_list = false; + if (ExpressionAnalyzer::analyze($statements_analyzer, $item->key, $context) === false) { return false; } @@ -190,6 +194,7 @@ class ArrayAnalyzer } else { $item_value_type = null; } + // if this array looks like an object-like array, let's return that instead if ($item_value_type && $item_key_type @@ -198,12 +203,24 @@ class ArrayAnalyzer ) { $object_like = new Type\Atomic\ObjectLike($property_types, $class_strings); $object_like->sealed = true; + $object_like->is_list = $all_list; $stmt->inferredType = new Type\Union([$object_like]); return null; } + if ($all_list) { + $array_type = new Type\Atomic\TNonEmptyList($item_value_type ?: Type::getMixed()); + $array_type->count = count($stmt->items); + + $stmt->inferredType = new Type\Union([ + $array_type, + ]); + + return null; + } + $array_type = new Type\Atomic\TNonEmptyArray([ $item_key_type ?: new Type\Union([new TInt, new TString]), $item_value_type ?: Type::getMixed(), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 66e994bf4..5a774183d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -9,7 +9,9 @@ use Psalm\Context; use Psalm\Type; use Psalm\Type\Atomic\ObjectLike; use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; use function array_reverse; use function array_shift; use function count; @@ -340,12 +342,18 @@ class ArrayAssignmentAnalyzer $new_child_type = $child_stmt->inferredType; // noop } } else { - $array_assignment_type = new Type\Union([ - new TArray([ - isset($current_dim->inferredType) ? $current_dim->inferredType : Type::getInt(), - $current_type, - ]), - ]); + if (!$current_dim) { + $array_assignment_type = new Type\Union([ + new TList($current_type), + ]); + } else { + $array_assignment_type = new Type\Union([ + new TArray([ + $current_dim->inferredType ?? Type::getMixed(), + $current_type, + ]), + ]); + } $new_child_type = Type::combineUnionTypes( $child_stmt->inferredType, @@ -436,29 +444,44 @@ class ArrayAssignmentAnalyzer } else { $array_atomic_key_type = Type::getMixed(); } + + $array_atomic_type = new TNonEmptyArray([ + $array_atomic_key_type, + $current_type, + ]); } else { - // todo: this can be improved I think - $array_atomic_key_type = Type::getInt(); + $array_atomic_type = new TNonEmptyList($current_type); } - $array_atomic_type = new TNonEmptyArray([ - $array_atomic_key_type, - $current_type, - ]); - $from_countable_object_like = false; + $new_child_type = null; + if (!$current_dim && !$context->inside_loop) { $atomic_root_types = $root_type->getTypes(); if (isset($atomic_root_types['array'])) { - if ($atomic_root_types['array'] instanceof TNonEmptyArray) { + if ($atomic_root_types['array'] instanceof TNonEmptyArray + || $atomic_root_types['array'] instanceof TNonEmptyList + ) { $array_atomic_type->count = $atomic_root_types['array']->count; } elseif ($atomic_root_types['array'] instanceof ObjectLike && $atomic_root_types['array']->sealed ) { $array_atomic_type->count = count($atomic_root_types['array']->properties); $from_countable_object_like = true; + + if ($atomic_root_types['array']->is_list + && $array_atomic_type instanceof TList + ) { + $array_atomic_type = clone $atomic_root_types['array']; + + $new_child_type = new Type\Union([$array_atomic_type]); + } + } elseif ($array_atomic_type instanceof TList) { + $array_atomic_type = new TNonEmptyList( + $array_atomic_type->type_param + ); } else { $array_atomic_type = new TNonEmptyArray( $array_atomic_type->type_params @@ -471,13 +494,15 @@ class ArrayAssignmentAnalyzer $array_atomic_type, ]); - $new_child_type = Type::combineUnionTypes( - $root_type, - $array_assignment_type, - $codebase, - true, - false - ); + if (!$new_child_type) { + $new_child_type = Type::combineUnionTypes( + $root_type, + $array_assignment_type, + $codebase, + true, + false + ); + } if ($from_countable_object_like) { $atomic_root_types = $new_child_type->getTypes(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index bb5dc1aec..79a911002 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -476,7 +476,7 @@ class AssignmentAnalyzer ) { /** * @psalm-suppress PossiblyUndefinedArrayOffset - * @var Type\Atomic\ObjectLike|Type\Atomic\TArray|null + * @var Type\Atomic\ObjectLike|Type\Atomic\TList|Type\Atomic\TArray|null */ $array_value_type = isset($assign_value_type->getTypes()['array']) ? $assign_value_type->getTypes()['array'] @@ -486,6 +486,13 @@ class AssignmentAnalyzer $array_value_type = $array_value_type->getGenericArrayType(); } + if ($array_value_type instanceof Type\Atomic\TList) { + $array_value_type = new Type\Atomic\TArray([ + Type::getInt(), + $array_value_type->type_param + ]); + } + self::analyze( $statements_analyzer, $var, @@ -543,6 +550,8 @@ class AssignmentAnalyzer if ($array_atomic_type instanceof Type\Atomic\TArray) { $new_assign_type = clone $array_atomic_type->type_params[1]; + } elseif ($array_atomic_type instanceof Type\Atomic\TList) { + $new_assign_type = clone $array_atomic_type->type_param; } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) { if ($assign_var_item->key && ($assign_var_item->key instanceof PhpParser\Node\Scalar\String_ diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index aa97e0900..e35138154 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -26,6 +26,7 @@ use Psalm\Type\Atomic\ObjectLike; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TMixed; @@ -1072,19 +1073,34 @@ class BinaryOpAnalyzer || $right_type_part instanceof TArray || $left_type_part instanceof ObjectLike || $right_type_part instanceof ObjectLike + || $left_type_part instanceof TList + || $right_type_part instanceof TList ) { - if ((!$right_type_part instanceof TArray && !$right_type_part instanceof ObjectLike) - || (!$left_type_part instanceof TArray && !$left_type_part instanceof ObjectLike) + if ((!$right_type_part instanceof TArray + && !$right_type_part instanceof ObjectLike + && !$right_type_part instanceof TList) + || (!$left_type_part instanceof TArray + && !$left_type_part instanceof ObjectLike + && !$left_type_part instanceof TList) ) { - if (!$left_type_part instanceof TArray && !$left_type_part instanceof ObjectLike) { + if (!$left_type_part instanceof TArray + && !$left_type_part instanceof ObjectLike + && !$left_type_part instanceof TList + ) { $invalid_left_messages[] = 'Cannot add an array to a non-array ' . $left_type_part; } else { $invalid_right_messages[] = 'Cannot add an array to a non-array ' . $right_type_part; } - if ($left_type_part instanceof TArray || $left_type_part instanceof ObjectLike) { + if ($left_type_part instanceof TArray + || $left_type_part instanceof ObjectLike + || $left_type_part instanceof TList + ) { $has_valid_left_operand = true; - } elseif ($right_type_part instanceof TArray || $right_type_part instanceof ObjectLike) { + } elseif ($right_type_part instanceof TArray + || $right_type_part instanceof ObjectLike + || $right_type_part instanceof TList + ) { $has_valid_right_operand = true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index f3d471467..db5f6ffb6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -41,8 +41,10 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TEmpty; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; use function strtolower; use function strpos; use function explode; @@ -734,7 +736,7 @@ class CallAnalyzer ) { /** * @psalm-suppress PossiblyUndefinedArrayOffset - * @var TArray|ObjectLike + * @var TArray|TList|ObjectLike */ $array_type = $arg->value->inferredType->getTypes()['array']; @@ -742,6 +744,10 @@ class CallAnalyzer $array_type = $array_type->getGenericArrayType(); } + if ($array_type instanceof TList) { + $array_type = new TArray([Type::getInt(), $array_type->type_param]); + } + if (in_array($method_id, ['shuffle', 'sort', 'rsort', 'usort'], true)) { $tvalue = $array_type->type_params[1]; $by_ref_type = new Type\Union([new TArray([Type::getInt(), clone $tvalue])]); @@ -1059,6 +1065,23 @@ class CallAnalyzer $array_atomic_type = new TArray($array_atomic_type->type_params); } + $array_type->addType($array_atomic_type); + } elseif ($array_atomic_type instanceof TNonEmptyList) { + if (!$context->inside_loop && $array_atomic_type->count !== null) { + if ($array_atomic_type->count === 0) { + $array_atomic_type = new TArray( + [ + new Type\Union([new TEmpty]), + new Type\Union([new TEmpty]), + ] + ); + } else { + $array_atomic_type->count--; + } + } else { + $array_atomic_type = new TList($array_atomic_type->type_param); + } + $array_type->addType($array_atomic_type); } } @@ -1677,13 +1700,17 @@ class CallAnalyzer if ($arg_type->hasArray()) { /** * @psalm-suppress PossiblyUndefinedArrayOffset - * @var Type\Atomic\TArray|Type\Atomic\ObjectLike + * @var Type\Atomic\TArray|Type\Atomic\TList|Type\Atomic\ObjectLike */ $array_atomic_type = $arg_type->getTypes()['array']; + if ($array_atomic_type instanceof Type\Atomic\ObjectLike) { - $array_atomic_type = $array_atomic_type->getGenericArrayType(); + $arg_type_param = $array_atomic_type->getGenericValueType(); + } elseif ($array_atomic_type instanceof Type\Atomic\TList) { + $arg_type_param = $array_atomic_type->type_param; + } else { + $arg_type_param = $array_atomic_type->type_params[1]; } - $arg_type_param = $array_atomic_type->type_params[1]; } else { $arg_type_param = Type::getMixed(); } @@ -1756,15 +1783,17 @@ class CallAnalyzer if ($arg_type->hasArray()) { /** * @psalm-suppress PossiblyUndefinedArrayOffset - * @var Type\Atomic\TArray|Type\Atomic\ObjectLike + * @var Type\Atomic\TArray|Type\Atomic\TList|Type\Atomic\ObjectLike */ $array_atomic_type = $arg_type->getTypes()['array']; if ($array_atomic_type instanceof Type\Atomic\ObjectLike) { - $array_atomic_type = $array_atomic_type->getGenericArrayType(); + $arg_type = $array_atomic_type->getGenericValueType(); + } elseif ($array_atomic_type instanceof Type\Atomic\TList) { + $arg_type = $array_atomic_type->type_param; + } else { + $arg_type = $array_atomic_type->type_params[1]; } - - $arg_type = $array_atomic_type->type_params[1]; } else { foreach ($arg_type->getTypes() as $atomic_type) { if (!$atomic_type->isIterable($codebase)) { @@ -1908,7 +1937,7 @@ class CallAnalyzer /** * @psalm-suppress PossiblyUndefinedArrayOffset - * @var ObjectLike|TArray|null + * @var ObjectLike|TArray|TList|null */ $array_arg_type = $array_arg && isset($array_arg->inferredType) @@ -1921,6 +1950,10 @@ class CallAnalyzer $array_arg_type = $array_arg_type->getGenericArrayType(); } + if ($array_arg_type instanceof TList) { + $array_arg_type = new TArray([Type::getInt(), $array_arg_type->type_param]); + } + $array_arg_types[] = $array_arg_type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 06fbbd27d..3c8fc1e90 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -35,6 +35,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; @@ -455,37 +456,51 @@ class ArrayFetchAnalyzer continue; } - if ($type instanceof TArray || $type instanceof ObjectLike) { + if ($type instanceof TArray || $type instanceof ObjectLike || $type instanceof TList) { $has_array_access = true; if ($in_assignment && $type instanceof TArray && (($type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty()) || ($type->type_params[1]->isMixed() && \is_string($key_value))) - && $key_value !== null ) { - $from_mixed_array = $type->type_params[1]->isMixed(); $from_empty_array = $type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty(); - $previous_key_type = $type->type_params[0]; - $previous_value_type = $type->type_params[1]; + if ($key_value !== null) { + $from_mixed_array = $type->type_params[1]->isMixed(); - // ok, type becomes an ObjectLike - $array_type->removeType($type_string); - $type = new ObjectLike([$key_value => $from_mixed_array ? Type::getMixed() : Type::getEmpty()]); + $previous_key_type = $type->type_params[0]; + $previous_value_type = $type->type_params[1]; - $type->sealed = $from_empty_array; + // ok, type becomes an ObjectLike + $array_type->removeType($type_string); + $type = new ObjectLike([$key_value => $from_mixed_array ? Type::getMixed() : Type::getEmpty()]); - if (!$from_empty_array) { - $type->previous_value_type = clone $previous_value_type; - $type->previous_key_type = clone $previous_key_type; + $type->sealed = $from_empty_array; + + if (!$from_empty_array) { + $type->previous_value_type = clone $previous_value_type; + $type->previous_key_type = clone $previous_key_type; + } + + $array_type->addType($type); + } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { + $array_type->removeType($type_string); + $array_type->addType(new Type\Atomic\TNonEmptyList($replacement_type)); + continue; } - - $array_type->addType($type); } $offset_type = self::replaceOffsetTypeWithInts($offset_type); + if ($type instanceof TList + && (($in_assignment && $stmt->dim) + || $original_type instanceof TTemplateParam + || !$offset_type->isInt()) + ) { + $type = new TArray([Type::getInt(), $type->type_param]); + } + if ($type instanceof TArray) { // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { @@ -634,9 +649,52 @@ class ArrayFetchAnalyzer $array_access_type = Type::getMixed(true); } } + } elseif ($type instanceof TList) { + // if we're assigning to an empty array with a key offset, refashion that array + if (!$in_assignment) { + $expected_offset_type = Type::getInt(); + + if ($codebase->config->ensure_array_int_offsets_exist) { + self::checkLiteralIntArrayOffset( + $offset_type, + $expected_offset_type, + $array_var_id, + $stmt, + $context, + $statements_analyzer + ); + } + + $has_valid_offset = true; + } + + if ($in_assignment && $type instanceof Type\Atomic\TNonEmptyList && $type->count !== null) { + $type->count++; + } + + if ($in_assignment && $replacement_type) { + $type->type_param = Type::combineUnionTypes( + $type->type_param, + $replacement_type, + $codebase + ); + } + + if (!$array_access_type) { + $array_access_type = $type->type_param; + } else { + $array_access_type = Type::combineUnionTypes( + $array_access_type, + $type->type_param + ); + } } else { $generic_key_type = $type->getGenericKeyType(); + if (!$stmt->dim && $type->sealed && $type->is_list) { + $key_value = count($type->properties); + } + if ($key_value !== null) { if (isset($type->properties[$key_value]) || $replacement_type) { $has_valid_offset = true; diff --git a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php index 2f8ca3e06..6535934e6 100644 --- a/src/Psalm/Internal/Analyzer/TypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/TypeAnalyzer.php @@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TEmptyMixed; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\GetClassT; use Psalm\Type\Atomic\GetTypeT; @@ -31,6 +32,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; @@ -1029,9 +1031,14 @@ class TypeAnalyzer } if ($container_type_part instanceof TIterable) { - if ($input_type_part instanceof TArray || $input_type_part instanceof ObjectLike) { + if ($input_type_part instanceof TArray + || $input_type_part instanceof ObjectLike + || $input_type_part instanceof TList + ) { if ($input_type_part instanceof ObjectLike) { $input_type_part = $input_type_part->getGenericArrayType(); + } elseif ($input_type_part instanceof TList) { + $input_type_part = new TArray([Type::getInt(), $input_type_part->type_param]); } $all_types_contain = true; @@ -1867,8 +1874,45 @@ class TypeAnalyzer } } - if (($input_type_part instanceof TArray || $input_type_part instanceof ObjectLike) - && ($container_type_part instanceof TArray || $container_type_part instanceof ObjectLike) + if ($container_type_part instanceof TList + && $input_type_part instanceof ObjectLike + && $input_type_part->is_list + ) { + $input_type_part = $input_type_part->getList(); + } + + if ($container_type_part instanceof TList + && $input_type_part instanceof TArray + && $input_type_part->type_params[1]->isEmpty() + ) { + return !$container_type_part instanceof TNonEmptyList; + } + + if ($input_type_part instanceof TList + && $container_type_part instanceof TList + ) { + if (!self::isContainedBy( + $codebase, + $input_type_part->type_param, + $container_type_part->type_param, + $input_type_part->type_param->ignore_nullable_issues, + $input_type_part->type_param->ignore_falsable_issues, + $atomic_comparison_result, + $allow_interface_equality + )) { + return false; + } + + return $input_type_part instanceof TNonEmptyList + || !$container_type_part instanceof TNonEmptyList; + } + + if (($input_type_part instanceof TArray + || $input_type_part instanceof ObjectLike + || $input_type_part instanceof TList) + && ($container_type_part instanceof TArray + || $container_type_part instanceof ObjectLike + || $container_type_part instanceof TList) ) { if ($container_type_part instanceof ObjectLike) { $generic_container_type_part = $container_type_part->getGenericArrayType(); @@ -1887,6 +1931,7 @@ class TypeAnalyzer ); if (!$input_type_part instanceof ObjectLike + && !$input_type_part instanceof TList && !$input_type_part->type_params[0]->hasMixed() && !($input_type_part->type_params[1]->isEmpty() && $container_params_can_be_undefined) @@ -1902,6 +1947,17 @@ class TypeAnalyzer $input_type_part = $input_type_part->getGenericArrayType(); } + if ($container_type_part instanceof TList) { + $all_types_contain = false; + $atomic_comparison_result->type_coerced = true; + + $container_type_part = new TArray([Type::getInt(), clone $container_type_part->type_param]); + } + + if ($input_type_part instanceof TList) { + $input_type_part = new TArray([Type::getInt(), clone $input_type_part->type_param]); + } + $any_scalar_param_match = false; foreach ($input_type_part->type_params as $i => $input_param) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 3b09195f9..f9a17fe79 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -44,6 +44,11 @@ class ArrayColumnReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn if ($row_type->isSingle() && $row_type->hasArray()) { $row_shape = $row_type->getTypes()['array']; } + } elseif ($input_array instanceof Type\Atomic\TList) { + $row_type = $input_array->type_param; + if ($row_type->isSingle() && $row_type->hasArray()) { + $row_shape = $row_type->getTypes()['array']; + } } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 2eb66f22c..b31a02c44 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -39,8 +39,9 @@ class ArrayFilterReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn && isset($array_arg->inferredType) && $array_arg->inferredType->hasType('array') && ($array_atomic_type = $array_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -51,6 +52,9 @@ class ArrayFilterReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn if ($first_arg_array instanceof Type\Atomic\TArray) { $inner_type = $first_arg_array->type_params[1]; $key_type = clone $first_arg_array->type_params[0]; + } elseif ($first_arg_array instanceof Type\Atomic\TList) { + $inner_type = $first_arg_array->type_param; + $key_type = Type::getInt(); } else { $inner_type = $first_arg_array->getGenericValueType(); $key_type = $first_arg_array->getGenericKeyType(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 274edc305..bebf699ea 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -43,7 +43,8 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp if (isset($arg_types['array']) && ($arg_types['array'] instanceof Type\Atomic\TArray - || $arg_types['array'] instanceof Type\Atomic\ObjectLike) + || $arg_types['array'] instanceof Type\Atomic\ObjectLike + || $arg_types['array'] instanceof Type\Atomic\TList) ) { $array_arg_type = $arg_types['array']; } @@ -55,6 +56,8 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp if (count($call_args) === 2) { if ($array_arg_type instanceof Type\Atomic\ObjectLike) { $generic_key_type = $array_arg_type->getGenericKeyType(); + } elseif ($array_arg_type instanceof Type\Atomic\TList) { + $generic_key_type = Type::getInt(); } else { $generic_key_type = $array_arg_type ? clone $array_arg_type->type_params[0] : Type::getArrayKey(); } @@ -90,6 +93,22 @@ class ArrayMapReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp ]); } + if ($array_arg_type instanceof Type\Atomic\TList) { + if ($array_arg_type instanceof Type\Atomic\TNonEmptyList) { + return new Type\Union([ + new Type\Atomic\TNonEmptyList( + $inner_type, + ), + ]); + } + + return new Type\Union([ + new Type\Atomic\TList( + $inner_type, + ), + ]); + } + if ($array_arg_type instanceof Type\Atomic\TNonEmptyArray) { return new Type\Union([ new Type\Atomic\TNonEmptyArray([ diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index a5ccf60fc..b54dfdbd6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -33,6 +33,8 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT $codebase = $statements_source->getCodebase(); $generic_properties = []; + $all_lists = true; + $all_nonempty_lists = true; foreach ($call_args as $call_arg) { if (!isset($call_arg->value->inferredType)) { @@ -44,6 +46,8 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT if (!$type_part instanceof Type\Atomic\TArray) { if ($type_part instanceof Type\Atomic\ObjectLike) { $type_part_value_type = $type_part->getGenericValueType(); + } elseif ($type_part instanceof Type\Atomic\TList) { + $type_part_value_type = $type_part->type_param; } else { return Type::getArray(); } @@ -71,6 +75,12 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT } $unpacked_type_part = $unpacked_type_part->getGenericArrayType(); + } elseif ($unpacked_type_part instanceof Type\Atomic\TList) { + $generic_properties = null; + + if (!$unpacked_type_part instanceof Type\Atomic\TNonEmptyList) { + $all_nonempty_lists = false; + } } else { if ($unpacked_type_part instanceof Type\Atomic\TMixed && $unpacked_type_part->from_loop_isset @@ -87,17 +97,25 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT $generic_properties = null; } - if ($unpacked_type_part->type_params[1]->isEmpty()) { - continue; + if ($unpacked_type_part instanceof Type\Atomic\TArray) { + if ($unpacked_type_part->type_params[1]->isEmpty()) { + continue; + } + + $all_lists = false; } $inner_key_types = array_merge( $inner_key_types, - array_values($unpacked_type_part->type_params[0]->getTypes()) + $unpacked_type_part instanceof Type\Atomic\TList + ? [new Type\Atomic\TInt()] + : array_values($unpacked_type_part->type_params[0]->getTypes()) ); $inner_value_types = array_merge( $inner_value_types, - array_values($unpacked_type_part->type_params[1]->getTypes()) + $unpacked_type_part instanceof Type\Atomic\TList + ? array_values($unpacked_type_part->type_param->getTypes()) + : array_values($unpacked_type_part->type_params[1]->getTypes()) ); } } @@ -110,10 +128,26 @@ class ArrayMergeReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT } if ($inner_value_types) { + $inner_value_type = TypeCombination::combineTypes($inner_value_types, $codebase, true); + + if ($all_lists) { + if ($all_nonempty_lists) { + return new Type\Union([ + new Type\Atomic\TNonEmptyList($inner_value_type), + ]); + } + + return new Type\Union([ + new Type\Atomic\TList($inner_value_type), + ]); + } + + $inner_key_type = TypeCombination::combineTypes($inner_key_types, $codebase, true); + return new Type\Union([ new Type\Atomic\TArray([ - TypeCombination::combineTypes($inner_key_types, $codebase, true), - TypeCombination::combineTypes($inner_value_types, $codebase, true), + $inner_key_type, + $inner_value_type, ]), ]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index d56467109..2c858d298 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -30,8 +30,9 @@ class ArrayPointerAdjustmentReturnTypeProvider implements \Psalm\Plugin\Hook\Fun && isset($first_arg->inferredType) && $first_arg->inferredType->hasType('array') && ($array_atomic_type = $first_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -41,6 +42,8 @@ class ArrayPointerAdjustmentReturnTypeProvider implements \Psalm\Plugin\Hook\Fun if ($first_arg_array instanceof Type\Atomic\TArray) { $value_type = clone $first_arg_array->type_params[1]; + } elseif ($first_arg_array instanceof Type\Atomic\TList) { + $value_type = clone $first_arg_array->type_param; } else { $value_type = $first_arg_array->getGenericValueType(); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 853ec2431..0c3c8d737 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -30,8 +30,9 @@ class ArrayPopReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp && isset($first_arg->inferredType) && $first_arg->inferredType->hasType('array') && ($array_atomic_type = $first_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -51,6 +52,12 @@ class ArrayPopReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTyp if (!$first_arg_array instanceof Type\Atomic\TNonEmptyArray) { $nullable = true; } + } elseif ($first_arg_array instanceof Type\Atomic\TList) { + $value_type = clone $first_arg_array->type_param; + + if (!$first_arg_array instanceof Type\Atomic\TNonEmptyList) { + $nullable = true; + } } else { $value_type = $first_arg_array->getGenericValueType(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php index b040755ef..7cbf52e69 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayRandReturnTypeProvider.php @@ -31,8 +31,9 @@ class ArrayRandReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTy && isset($first_arg->inferredType) && $first_arg->inferredType->hasType('array') && ($array_atomic_type = $first_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -42,6 +43,8 @@ class ArrayRandReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTy if ($first_arg_array instanceof Type\Atomic\TArray) { $key_type = clone $first_arg_array->type_params[0]; + } elseif ($first_arg_array instanceof Type\Atomic\TList) { + $key_type = clone $first_arg_array->type_param; } else { $key_type = $first_arg_array->getGenericKeyType(); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index fe64d2525..69fcac447 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -53,12 +53,15 @@ class ArrayReduceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturn if (isset($array_arg_types['array']) && ($array_arg_types['array'] instanceof Type\Atomic\TArray - || $array_arg_types['array'] instanceof Type\Atomic\ObjectLike) + || $array_arg_types['array'] instanceof Type\Atomic\ObjectLike + || $array_arg_types['array'] instanceof Type\Atomic\TList) ) { $array_arg_type = $array_arg_types['array']; if ($array_arg_type instanceof Type\Atomic\ObjectLike) { $array_arg_type = $array_arg_type->getGenericArrayType(); + } elseif ($array_arg_type instanceof Type\Atomic\TList) { + $array_arg_type = new Type\Atomic\TArray([Type::getInt(), clone $array_arg_type->type_param]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php index 8ce9ed8a7..58c606ad3 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReverseReturnTypeProvider.php @@ -24,14 +24,15 @@ class ArrayReverseReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionRetur Context $context, CodeLocation $code_location ) : Type\Union { - $first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null; + $first_arg = $call_args[0]->value ?? null; $first_arg_array = $first_arg && isset($first_arg->inferredType) && $first_arg->inferredType->hasType('array') && ($array_atomic_type = $first_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -43,6 +44,18 @@ class ArrayReverseReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionRetur return new Type\Union([clone $first_arg_array]); } + if ($first_arg_array instanceof Type\Atomic\TList) { + $second_arg = $call_args[1]->value ?? null; + + if (!$second_arg + || (isset($second_arg->inferredType) && $second_arg->inferredType->isFalse()) + ) { + return new Type\Union([clone $first_arg_array]); + } + + return new Type\Union([new Type\Atomic\TArray([Type::getInt(), clone $first_arg_array->type_param])]); + } + return new Type\Union([$first_arg_array->getGenericArrayType()]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php index 7cf7e4a22..8a787ccd6 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArraySliceReturnTypeProvider.php @@ -31,8 +31,9 @@ class ArraySliceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT && isset($first_arg->inferredType) && $first_arg->inferredType->hasType('array') && ($array_atomic_type = $first_arg->inferredType->getTypes()['array']) - && ($array_atomic_type instanceof Type\Atomic\TArray || - $array_atomic_type instanceof Type\Atomic\ObjectLike) + && ($array_atomic_type instanceof Type\Atomic\TArray + || $array_atomic_type instanceof Type\Atomic\ObjectLike + || $array_atomic_type instanceof Type\Atomic\TList) ? $array_atomic_type : null; @@ -44,6 +45,10 @@ class ArraySliceReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnT return new Type\Union([clone $first_arg_array]); } + if ($first_arg_array instanceof Type\Atomic\TList) { + return new Type\Union([new Type\Atomic\TArray([Type::getInt(), clone $first_arg_array->type_param])]); + } + return new Type\Union([$first_arg_array->getGenericArrayType()]); } } diff --git a/src/Psalm/Internal/Stubs/CoreGenericFunctions.php b/src/Psalm/Internal/Stubs/CoreGenericFunctions.php index ff93935a5..946f38d99 100644 --- a/src/Psalm/Internal/Stubs/CoreGenericFunctions.php +++ b/src/Psalm/Internal/Stubs/CoreGenericFunctions.php @@ -6,7 +6,7 @@ * @param mixed $search_value * @param bool $strict * - * @return array + * @return list * @psalm-pure */ function array_keys(array $arr, $search_value = null, bool $strict = false) @@ -18,7 +18,7 @@ function array_keys(array $arr, $search_value = null, bool $strict = false) * * @param array $arr * - * @return array + * @return list * @psalm-pure */ function array_values(array $arr) diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 75f490449..4573d6e81 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -35,6 +35,7 @@ use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TEmpty; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; @@ -782,7 +783,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler $array_atomic_type = $existing_var_type->getTypes()['array']; $did_remove_type = false; - if ($array_atomic_type instanceof Type\Atomic\TArray + if ($array_atomic_type instanceof TArray && !$array_atomic_type instanceof Type\Atomic\TNonEmptyArray ) { $did_remove_type = true; @@ -795,6 +796,15 @@ class AssertionReconciler extends \Psalm\Type\Reconciler ) ); } + } elseif ($array_atomic_type instanceof TList + && !$array_atomic_type instanceof Type\Atomic\TNonEmptyList + ) { + $did_remove_type = true; + $existing_var_type->addType( + new Type\Atomic\TNonEmptyList( + $array_atomic_type->type_param + ) + ); } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike) { foreach ($array_atomic_type->properties as $property_type) { if ($property_type->possibly_undefined) { @@ -1528,6 +1538,8 @@ class AssertionReconciler extends \Psalm\Type\Reconciler if ($type->isArrayAccessibleWithStringKey($codebase)) { if (get_class($type) === TArray::class) { $array_types[] = new Atomic\TNonEmptyArray($type->type_params); + } elseif (get_class($type) === TList::class) { + $array_types[] = new Atomic\TNonEmptyList($type->type_param); } else { $array_types[] = $type; } @@ -1844,6 +1856,7 @@ class AssertionReconciler extends \Psalm\Type\Reconciler $array_atomic_type = $existing_var_atomic_types['array']; if ($array_atomic_type instanceof Type\Atomic\TNonEmptyArray + || $array_atomic_type instanceof Type\Atomic\TNonEmptyList || ($array_atomic_type instanceof Type\Atomic\ObjectLike && array_filter( $array_atomic_type->properties, diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index 9a2ce4400..6cdbb21f0 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -443,6 +443,7 @@ class NegatedAssertionReconciler extends Reconciler $did_remove_type = false; if ($array_atomic_type instanceof Type\Atomic\TNonEmptyArray + || $array_atomic_type instanceof Type\Atomic\TNonEmptyList || ($array_atomic_type instanceof Type\Atomic\ObjectLike && $array_atomic_type->sealed) ) { $did_remove_type = true; @@ -923,6 +924,16 @@ class NegatedAssertionReconciler extends Reconciler ) ); } + } elseif ($array_atomic_type instanceof Type\Atomic\TList + && !$array_atomic_type instanceof Type\Atomic\TNonEmptyList + ) { + $did_remove_type = true; + + $existing_var_type->addType( + new Type\Atomic\TNonEmptyList( + $array_atomic_type->type_param + ) + ); } elseif ($array_atomic_type instanceof Type\Atomic\ObjectLike && !$array_atomic_type->sealed ) { diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index fb822e34e..174fde873 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -28,6 +28,7 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -35,6 +36,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyMixed; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; @@ -114,6 +116,9 @@ class TypeCombination */ private $extra_types; + /** @var ?bool */ + private $all_arrays_lists; + /** * Combines types together * - so `int + string = int|string` @@ -123,7 +128,7 @@ class TypeCombination * - and `array + array = array` * - and `array + array = array` * - * @param array $types + * @param list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar * @@ -321,6 +326,10 @@ class TypeCombination $objectlike->previous_value_type = $combination->objectlike_value_type; } + if ($combination->all_arrays_lists) { + $objectlike->is_list = true; + } + $new_types[] = $objectlike; } else { $new_types[] = new Type\Atomic\TArray([Type::getArrayKey(), Type::getMixed()]); @@ -410,14 +419,25 @@ class TypeCombination && $combination->objectlike_sealed && $overwrite_empty_array) ) { - if ($combination->array_counts && count($combination->array_counts) === 1) { - $array_type = new TNonEmptyArray($generic_type_params); - $array_type->count = array_keys($combination->array_counts)[0]; + if ($combination->all_arrays_lists) { + $array_type = new TNonEmptyList($generic_type_params[1]); + + if ($combination->array_counts && count($combination->array_counts) === 1) { + $array_type->count = array_keys($combination->array_counts)[0]; + } } else { $array_type = new TNonEmptyArray($generic_type_params); + + if ($combination->array_counts && count($combination->array_counts) === 1) { + $array_type->count = array_keys($combination->array_counts)[0]; + } } } else { - $array_type = new TArray($generic_type_params); + if ($combination->all_arrays_lists) { + $array_type = new TList($generic_type_params[1]); + } else { + $array_type = new TArray($generic_type_params); + } } $new_types[] = $array_type; @@ -671,6 +691,41 @@ class TypeCombination } else { $combination->array_always_filled = false; } + + if (!$type->type_params[1]->isEmpty()) { + $combination->all_arrays_lists = false; + } + } elseif ($type instanceof TList) { + foreach ([Type::getInt(), $type->type_param] as $i => $type_param) { + if (isset($combination->array_type_params[$i])) { + $combination->array_type_params[$i] = Type::combineUnionTypes( + $combination->array_type_params[$i], + $type_param, + $codebase, + $overwrite_empty_array + ); + } else { + $combination->array_type_params[$i] = $type_param; + } + } + + if ($type instanceof TNonEmptyList) { + if ($combination->array_counts !== null) { + if ($type->count === null) { + $combination->array_counts = null; + } else { + $combination->array_counts[$type->count] = true; + } + } + + $combination->array_sometimes_filled = true; + } else { + $combination->array_always_filled = false; + } + + if ($combination->all_arrays_lists !== false) { + $combination->all_arrays_lists = true; + } } elseif (($type instanceof TGenericObject && ($type->value === 'Traversable' || $type->value === 'Generator')) || ($type instanceof TIterable && $type->has_docblock_params) || ($type instanceof TArray && $type_key === 'iterable') @@ -759,8 +814,14 @@ class TypeCombination $combination->array_counts[count($type->properties)] = true; } - foreach ($possibly_undefined_entries as $type) { - $type->possibly_undefined = true; + foreach ($possibly_undefined_entries as $possibly_undefined_type) { + $possibly_undefined_type->possibly_undefined = true; + } + + if (!$type->is_list) { + $combination->all_arrays_lists = false; + } elseif ($combination->all_arrays_lists !== false) { + $combination->all_arrays_lists = true; } } else { if ($type instanceof TObject) { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index e174375a2..f77213444 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -36,12 +36,14 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIterable; +use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TObject; @@ -103,6 +105,7 @@ abstract class Type 'key-of' => true, 'value-of' => true, 'non-empty-countable' => true, + 'list' => true, ]; /** @@ -276,6 +279,14 @@ abstract class Type return new TIterable($generic_params); } + if ($generic_type_value === 'list') { + return new TList($generic_params[0]); + } + + if ($generic_type_value === 'non-empty-list') { + return new TNonEmptyList($generic_params[0]); + } + if ($generic_type_value === 'class-string') { $class_name = (string) $generic_params[0]; diff --git a/src/Psalm/Type/Algebra.php b/src/Psalm/Type/Algebra.php index 39d63fc94..1604e5d1c 100644 --- a/src/Psalm/Type/Algebra.php +++ b/src/Psalm/Type/Algebra.php @@ -297,7 +297,7 @@ class Algebra * * @param array $clauses * - * @return array + * @return list */ public static function simplifyCNF(array $clauses) { @@ -404,7 +404,7 @@ class Algebra /** * Look for clauses with only one possible value * - * @param array $clauses + * @param list $clauses * @param array $cond_referenced_var_ids * * @return array>> @@ -628,7 +628,7 @@ class Algebra * * @param array $clauses * - * @return array + * @return list */ public static function negateFormula(array $clauses, int $complexity = null) { diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index c33f6ce8c..fe14a59a5 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -464,6 +464,12 @@ class Reconciler if ($existing_key_type_part instanceof Type\Atomic\TArray) { $new_base_type_candidate = clone $existing_key_type_part->type_params[1]; + if ($has_isset) { + $new_base_type_candidate->possibly_undefined = true; + } + } elseif ($existing_key_type_part instanceof Type\Atomic\TList) { + $new_base_type_candidate = clone $existing_key_type_part->type_param; + if ($has_isset) { $new_base_type_candidate->possibly_undefined = true; } diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 05d8f9424..0a34229a5 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -659,6 +659,13 @@ class ArrayAccessTest extends TestCase /** @psalm-suppress MixedPropertyFetch */ print_r([&$a->foo->bar]);', ], + 'accessOffsetOnList' => [ + ' $arr */ + function foo(array $arr) : void { + echo $arr[3] ?? null; + }', + ], ]; } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index f6f17c316..33557ef98 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -42,7 +42,7 @@ class ArrayAssignmentTest extends TestCase $out[] = 4;', 'assertions' => [ - '$out' => 'non-empty-array', + '$out' => 'non-empty-list', ], ], 'genericArrayCreationWithInt' => [ @@ -53,7 +53,7 @@ class ArrayAssignmentTest extends TestCase $out[] = 4; }', 'assertions' => [ - '$out' => 'non-empty-array', + '$out' => 'non-empty-list', ], ], 'generic2dArrayCreation' => [ @@ -64,7 +64,7 @@ class ArrayAssignmentTest extends TestCase $out[] = [4]; }', 'assertions' => [ - '$out' => 'non-empty-array', + '$out' => 'non-empty-list', ], ], 'generic2dArrayCreationAddedInIf' => [ @@ -84,7 +84,7 @@ class ArrayAssignmentTest extends TestCase $out[] = $bits;', 'assertions' => [ - '$out' => 'non-empty-array>', + '$out' => 'non-empty-list>', ], ], 'genericArrayCreationWithObjectAddedInIf' => [ @@ -97,7 +97,7 @@ class ArrayAssignmentTest extends TestCase $out[] = new B(); }', 'assertions' => [ - '$out' => 'array', + '$out' => 'list', ], ], 'genericArrayCreationWithElementAddedInSwitch' => [ @@ -113,7 +113,7 @@ class ArrayAssignmentTest extends TestCase // do nothing }', 'assertions' => [ - '$out' => 'array', + '$out' => 'list', ], ], 'genericArrayCreationWithElementsAddedInSwitch' => [ @@ -130,7 +130,7 @@ class ArrayAssignmentTest extends TestCase break; }', 'assertions' => [ - '$out' => 'array', + '$out' => 'list', ], ], 'genericArrayCreationWithElementsAddedInSwitchWithNothing' => [ @@ -150,15 +150,7 @@ class ArrayAssignmentTest extends TestCase // do nothing }', 'assertions' => [ - '$out' => 'array', - ], - ], - 'implicitIntArrayCreation' => [ - ' [ - '$foo' => 'non-empty-array', + '$out' => 'list', ], ], 'implicit2dIntArrayCreation' => [ @@ -166,7 +158,7 @@ class ArrayAssignmentTest extends TestCase $foo = []; $foo[][] = "hello";', 'assertions' => [ - '$foo' => 'non-empty-array>', + '$foo' => 'non-empty-list>', ], ], 'implicit3dIntArrayCreation' => [ @@ -174,7 +166,7 @@ class ArrayAssignmentTest extends TestCase $foo = []; $foo[][][] = "hello";', 'assertions' => [ - '$foo' => 'non-empty-array>>', + '$foo' => 'non-empty-list>>', ], ], 'implicit4dIntArrayCreation' => [ @@ -182,7 +174,7 @@ class ArrayAssignmentTest extends TestCase $foo = []; $foo[][][][] = "hello";', 'assertions' => [ - '$foo' => 'non-empty-array>>>', + '$foo' => 'non-empty-list>>>', ], ], 'implicitIndexedIntArrayCreation' => [ @@ -296,9 +288,9 @@ class ArrayAssignmentTest extends TestCase $foo["b"][] = "goodbye"; $bar = $foo["a"];', 'assertions' => [ - '$foo' => 'array{a: string, b: array}', + '$foo' => 'array{a: string, b: non-empty-list}', '$foo[\'a\']' => 'string', - '$foo[\'b\']' => 'array', + '$foo[\'b\']' => 'non-empty-list', '$bar' => 'string', ], ], @@ -368,8 +360,8 @@ class ArrayAssignmentTest extends TestCase $c = []; $c[$b][$b][] = "bam";', 'assertions' => [ - '$a' => 'array{boop: array}', - '$c' => 'array{boop: non-empty-array>}', + '$a' => 'array{boop: non-empty-list}', + '$c' => 'array{boop: non-empty-array>}', ], ], 'assignExplicitValueToGeneric' => [ @@ -783,7 +775,7 @@ class ArrayAssignmentTest extends TestCase $a = null; $a[0][] = 1;', 'assertions' => [ - '$a' => 'array{0: array}', + '$a' => 'array{0: non-empty-list}', ], 'error_levels' => ['PossiblyNullArrayAssignment'], ], @@ -820,8 +812,8 @@ class ArrayAssignmentTest extends TestCase $a_keys = array_keys($a);', 'assertions' => [ '$a' => 'array{0: string, 1: int}', - '$a_values' => 'array', - '$a_keys' => 'array', + '$a_values' => 'list', + '$a_keys' => 'list', ], ], 'changeIntOffsetKeyValuesWithDirectAssignment' => [ @@ -871,7 +863,7 @@ class ArrayAssignmentTest extends TestCase $a = null; }', 'assertions' => [ - '$a' => 'non-empty-array|null', + '$a' => 'non-empty-list|null', ], ], 'assignArrayOrSetNullInElseIf' => [ @@ -887,7 +879,7 @@ class ArrayAssignmentTest extends TestCase $a = null; }', 'assertions' => [ - '$a' => 'array|null', + '$a' => 'list|null', ], ], 'assignArrayOrSetNullInElse' => [ @@ -903,7 +895,7 @@ class ArrayAssignmentTest extends TestCase $a = null; }', 'assertions' => [ - '$a' => 'non-empty-array|null', + '$a' => 'non-empty-list|null', ], ], 'mixedMethodCallArrayAccess' => [ @@ -1145,6 +1137,93 @@ class ArrayAssignmentTest extends TestCase return $array; }' ], + 'listUsedAsArray' => [ + ' [ + '$a' => 'non-empty-list' + ], + ], + 'listTakesEmptyArray' => [ + ' $arr */ + function takesList(array $arr) : void {} + + $a = []; + + takesList($a);', + 'assertions' => [ + '$a' => 'array' + ], + ], + 'listCreatedInSingleStatementUsedAsArray' => [ + ' $arr */ + function takesList(array $arr) : void {} + + $a = [1, 2]; + + takesArray($a); + takesList($a); + + $a[] = 3; + + takesArray($a); + takesList($a); + + $b = $a; + + $b[] = rand(0, 10);', + 'assertions' => [ + '$a' => 'array{0: int, 1: int, 2: int}', + '$b' => 'array{0: int, 1: int, 2: int, 3: int}', + ], + ], + 'listMergedWithObjectLikeList' => [ + ' $arr */ + function takesAnotherList(array $arr) : void {} + + /** @param list $arr */ + function takesList(array $arr) : void { + if (rand(0, 1)) { + $arr = [1, 2, 3]; + } + + takesAnotherList($arr); + }', + ], + 'listMergedWithObjectLikeListAfterAssertion' => [ + ' $arr */ + function takesAnotherList(array $arr) : void {} + + /** @param list $arr */ + function takesList(array $arr) : void { + if ($arr) { + $arr = [4, 5, 6]; + } + + takesAnotherList($arr); + }', + ], + 'nonEmptyAssertionOnListElement' => [ + '> $arr */ + function takesList(array $arr) : void { + if (!empty($arr[0])) { + foreach ($arr[0] as $k => $v) {} + } + }', + ], ]; } @@ -1313,6 +1392,30 @@ class ArrayAssignmentTest extends TestCase $storage[$key] = "test";', 'error_message' => 'InvalidArgument', ], + 'listUsedAsArrayWrongType' => [ + ' 'InvalidScalarArgument', + ], + 'listUsedAsArrayWrongListType' => [ + ' $arr */ + function takesArray(array $arr) : void {} + + $a = []; + $a[] = 1; + $a[] = 2; + + takesArray($a);', + 'error_message' => 'InvalidScalarArgument', + ], ]; } } diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 3543dfcdb..523f3043f 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -197,7 +197,7 @@ class FunctionCallTest extends TestCase ' 1, "b" => 2]);', 'assertions' => [ - '$a' => 'array', + '$a' => 'list', ], ], 'arrayKeysMixed' => [ @@ -206,15 +206,17 @@ class FunctionCallTest extends TestCase $b = ["a" => 5]; $a = array_keys($b);', 'assertions' => [ - '$a' => 'array', + '$a' => 'list', ], 'error_levels' => ['MixedArgument'], ], 'arrayValues' => [ ' 1, "b" => 2]);', + $b = array_values(["a" => 1, "b" => 2]); + $c = array_values(["a" => "hello", "b" => "jello"]);', 'assertions' => [ - '$b' => 'array', + '$b' => 'list', + '$c' => 'list', ], ], 'arrayCombine' => [ diff --git a/tests/Php56Test.php b/tests/Php56Test.php index ce1c4a5bf..2ff6a0462 100644 --- a/tests/Php56Test.php +++ b/tests/Php56Test.php @@ -94,7 +94,7 @@ class Php56Test extends TestCase array_push($a, ...$b);', 'assertions' => [ - '$a' => 'non-empty-array', + '$a' => 'non-empty-list', ], ], 'arrayMergeArgumentUnpacking' => [ diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 96aea484f..2575769af 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -13,21 +13,23 @@ class TypeCombinationTest extends TestCase * @dataProvider providerTestValidTypeCombination * * @param string $expected - * @param array $types + * @param list $types * * @return void */ public function testValidTypeCombination($expected, $types) { - foreach ($types as $k => $type) { - $types[$k] = self::getAtomic($type); - $types[$k]->setFromDocblock(); + $converted_types = []; + + foreach ($types as $type) { + $converted_type = self::getAtomic($type); + $converted_type->setFromDocblock(); + $converted_types[] = $converted_type; } - /** @psalm-suppress InvalidArgument */ $this->assertSame( $expected, - (string) TypeCombination::combineTypes($types) + (string) TypeCombination::combineTypes($converted_types) ); } @@ -58,7 +60,7 @@ class TypeCombinationTest extends TestCase } /** - * @return array}> + * @return array}> */ public function providerTestValidTypeCombination() {