diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index e9dbe849702..6e9e525ba52 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -779,8 +779,10 @@ public static function processFunctionCall( $class_string_type = new TClassString(); if ($class_exists_check_type === 1) { $class_string_type->is_loaded = true; + $if_types[$first_var_name] = [[new IsIdentical($class_string_type)]]; + } else { + $if_types[$first_var_name] = [[new IsType($class_string_type)]]; } - $if_types[$first_var_name] = [[new IsType($class_string_type)]]; } } elseif ($class_exists_check_type = self::hasTraitExistsCheck($expr)) { if ($first_var_name) { diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index ab2a8bc2f89..32476d2d378 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -182,7 +182,7 @@ public static function reconcile( $existing_var_type->removeType('array-key'); $existing_var_type->addType(new TString); } elseif ($assertion instanceof IsClassNotEqual) { - $assertion_type = Atomic::create($assertion->type); + // do nothing } elseif ($existing_var_type->isSingle() && $existing_var_type->hasNamedObjectType() && $assertion_type instanceof TNamedObject diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index d45af1cef99..c7770a32c26 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -38,6 +38,9 @@ class TypeCombination /** @var array|null */ public $array_counts = []; + /** @var array|null */ + public $array_min_counts = []; + /** @var bool */ public $array_sometimes_filled = false; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 5aad86c0510..edd502163d6 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -545,6 +545,14 @@ private static function scrapeTypeProperties( } } + if ($combination->array_min_counts !== null) { + if ($type->min_count === null) { + $combination->array_min_counts = null; + } else { + $combination->array_min_counts[$type->min_count] = true; + } + } + $combination->array_sometimes_filled = true; } else { $combination->array_always_filled = false; @@ -586,6 +594,14 @@ private static function scrapeTypeProperties( } } + if ($combination->array_min_counts !== null) { + if ($type->min_count === null) { + $combination->array_min_counts = null; + } else { + $combination->array_min_counts[$type->min_count] = true; + } + } + $combination->array_sometimes_filled = true; } else { $combination->array_always_filled = false; @@ -717,6 +733,16 @@ private static function scrapeTypeProperties( $combination->array_counts[count($type->properties)] = true; } + if ($combination->array_min_counts !== null) { + $min_prop_count = count( + array_filter( + $type->properties, + fn($p) => $p->possibly_undefined + ) + ); + $combination->array_min_counts[$min_prop_count] = true; + } + foreach ($possibly_undefined_entries as $possibly_undefined_type) { $possibly_undefined_type->possibly_undefined = true; } @@ -1483,6 +1509,11 @@ private static function getArrayTypeFromGenericParams( /** @psalm-suppress PropertyTypeCoercion */ $array_type->count = array_keys($combination->array_counts)[0]; } + + if ($combination->array_min_counts) { + /** @psalm-suppress PropertyTypeCoercion */ + $array_type->min_count = min(array_keys($combination->array_min_counts)); + } } } else { $array_type = new TNonEmptyArray($generic_type_params); @@ -1491,6 +1522,11 @@ private static function getArrayTypeFromGenericParams( /** @psalm-suppress PropertyTypeCoercion */ $array_type->count = array_keys($combination->array_counts)[0]; } + + if ($combination->array_min_counts) { + /** @psalm-suppress PropertyTypeCoercion */ + $array_type->min_count = min(array_keys($combination->array_min_counts)); + } } } else { if ($combination->all_arrays_class_string_maps diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 89d681d7e1c..eef2404dd00 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -317,6 +317,30 @@ function bar(array $list) : void { $this->analyzeFile('somefile.php', new Context()); } + public function testCountOnKeyedArrayInRangeWithUpdate(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + ' $list */ + function bar(array $list) : void { + if (rand(0, 1)) { + $list = ["a"]; + } + if (count($list) > 1) { + if ($list[1][0] === "a") { + $list[1] = "foo"; + } + echo $list[1]; + } + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } + public function testCountOnKeyedArrayOutOfRange(): void { Config::getInstance()->ensure_array_int_offsets_exist = true;