diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index 0f08619e0..f9a192648 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -35,7 +35,6 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index ac6bc7553..04301d8c4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -29,7 +29,6 @@ use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateIndexedAccess; use Psalm\Type\Atomic\TTemplateKeyOf; @@ -684,14 +683,34 @@ class ArrayAssignmentAnalyzer if ($from_countable_object_like) { $atomic_root_types = $new_child_type->getAtomicTypes(); - if (isset($atomic_root_types['array']) - && ($atomic_root_types['array'] instanceof TNonEmptyArray - || $atomic_root_types['array'] instanceof TNonEmptyList) - && $atomic_root_types['array']->count !== null - ) { - $atomic_root_types['array'] = - $atomic_root_types['array']->setCount($atomic_root_types['array']->count+1); - $new_child_type = new Union($atomic_root_types); + if (isset($atomic_root_types['array'])) { + if ($atomic_root_types['array'] instanceof TNonEmptyArray + && $atomic_root_types['array']->count !== null + ) { + $atomic_root_types['array'] = + $atomic_root_types['array']->setCount($atomic_root_types['array']->count+1); + $new_child_type = new Union($atomic_root_types); + } elseif ($atomic_root_types['array'] instanceof TKeyedArray + && $atomic_root_types['array']->is_list) { + $properties = $atomic_root_types['array']->properties; + $had_undefined = false; + foreach ($properties as &$property) { + if ($property->possibly_undefined) { + $property = $property->setPossiblyUndefined(true); + $had_undefined = true; + break; + } + } + + if (!$had_undefined && $atomic_root_types['array']->fallback_params) { + $properties []= $atomic_root_types['array']->fallback_params[1]; + } + + $atomic_root_types['array'] = + $atomic_root_types['array']->setProperties($properties); + + $new_child_type = new Union($atomic_root_types); + } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index dddd91ff6..63a2dc8c6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -36,7 +36,6 @@ use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use UnexpectedValueException; @@ -540,15 +539,25 @@ class ArrayFunctionArgumentsAnalyzer } $array_atomic_types[] = $array_atomic_type; - } elseif ($array_atomic_type instanceof TNonEmptyList) { - if (!$context->inside_loop && $array_atomic_type->count !== null) { - if ($array_atomic_type->count === 1) { - $array_atomic_type = Type::getListAtomic(Type::getNever()); + } elseif ($array_atomic_type instanceof TKeyedArray && $array_atomic_type->is_list) { + if (!$context->inside_loop + && ($prop_count = $array_atomic_type->getMaxCount()) + && $prop_count === $array_atomic_type->getMinCount() + ) { + if ($prop_count === 1) { + $array_atomic_type = new TArray( + [ + Type::getNever(), + Type::getNever(), + ] + ); } else { - $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); + $properties = $array_atomic_type->properties; + unset($properties[$prop_count-1]); + $array_atomic_type = $array_atomic_type->setProperties($properties); } } else { - $array_atomic_type = Type::getListAtomic($array_atomic_type->type_param); + $array_atomic_type = Type::getListAtomic($array_atomic_type->getGenericValueType()); } $array_atomic_types[] = $array_atomic_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ff793ea2e..1504be13a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -37,7 +37,6 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index ae99a2a9b..8bf81bbc4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -35,7 +35,6 @@ 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\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 825b6ed29..30cbf4323 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -9,7 +9,6 @@ use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use function count; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 2e17ffddb..e95108cb1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -11,7 +11,6 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNull; diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 97daa774a..810146100 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -41,7 +41,6 @@ use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 38e8d6662..443c52020 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -54,7 +54,6 @@ use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyLowercaseString; use Psalm\Type\Atomic\TNonEmptyMixed; use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString; @@ -607,12 +606,7 @@ class SimpleAssertionReconciler extends Reconciler } } elseif ($array_atomic_type instanceof TKeyedArray) { $prop_max_count = count($array_atomic_type->properties); - $prop_min_count = 0; - foreach ($array_atomic_type->properties as $property_type) { - if (!$property_type->possibly_undefined) { - $prop_min_count++; - } - } + $prop_min_count = $array_atomic_type->getMinCount(); if ($assertion instanceof HasAtLeastCount) { if ($array_atomic_type->fallback_params === null) { @@ -731,12 +725,7 @@ class SimpleAssertionReconciler extends Reconciler } } elseif ($array_atomic_type instanceof TKeyedArray) { $prop_max_count = count($array_atomic_type->properties); - $prop_min_count = 0; - foreach ($array_atomic_type->properties as $prop) { - if (!$prop->possibly_undefined) { - $prop_min_count++; - } - } + $prop_min_count = $array_atomic_type->getMinCount(); if ($assertion->count < $prop_min_count) { // Impossible diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 520c1bd7f..efd700cb4 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -45,8 +45,6 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNever; -use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyLowercaseString; use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString; use Psalm\Type\Atomic\TNonEmptyString; @@ -66,6 +64,8 @@ use function get_class; use function max; use function strpos; +use const INF; + /** * @internal */ @@ -527,12 +527,7 @@ class SimpleNegatedAssertionReconciler extends Reconciler $prop_max_count = $array_atomic_type->fallback_params === null ? count($array_atomic_type->properties) : INF; - $prop_min_count = 0; - foreach ($array_atomic_type->properties as $property_type) { - if (!$property_type->possibly_undefined) { - $prop_min_count++; - } - } + $prop_min_count = $array_atomic_type->getMinCount(); // !(count($a) >= 3) // count($a) < 3 diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index fe6556699..cafa7e459 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -370,6 +370,34 @@ class TKeyedArray extends Atomic return false; } + public function getMinCount(): int + { + if ($this->is_list) { + foreach ($this->properties as $k => $property) { + if ($property->possibly_undefined) { + return $k; + } + } + } + $prop_min_count = 0; + foreach ($this->properties as $k => $property) { + if (!$property->possibly_undefined) { + $prop_min_count++; + } + } + return $prop_min_count; + } + + /** + * Returns null if there is no upper limit. + */ + public function getMaxCount(): ?int + { + if ($this->fallback_params) { + return null; + } + return count($this->properties); + } /** * Whether all keys are always defined (ignores unsealedness). */