diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 576fe45a0..01150233a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -351,7 +351,9 @@ final class ArrayAssignmentAnalyzer if (!$has_matching_objectlike_property && !$has_matching_string) { $properties = []; $classStrings = []; - $current_type = $current_type->setPossiblyUndefined(count($key_values) > 1); + $current_type = $current_type->setPossiblyUndefined( + $current_type->possibly_undefined || count($key_values) > 1, + ); foreach ($key_values as $key_value) { $properties[$key_value->value] = $current_type; if ($key_value instanceof TLiteralClassString) { diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index 0706315d1..94e64793e 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -55,6 +55,9 @@ final class TypeCombination /** @var array */ public array $objectlike_entries = []; + /** @var array */ + public array $objectlike_class_string_keys = []; + public bool $objectlike_sealed = true; public ?Union $objectlike_key_type = null; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 3f841da4f..773e3f71a 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -667,6 +667,7 @@ final class TypeCombiner $has_defined_keys = false; + $class_strings = $type->class_strings ?? []; foreach ($type->properties as $candidate_property_name => $candidate_property_type) { $value_type = $combination->objectlike_entries[$candidate_property_name] ?? null; @@ -706,6 +707,19 @@ final class TypeCombiner } unset($missing_entries[$candidate_property_name]); + + if (is_int($candidate_property_name)) { + continue; + } + + if (isset($combination->objectlike_class_string_keys[$candidate_property_name])) { + $combination->objectlike_class_string_keys[$candidate_property_name] = + $combination->objectlike_class_string_keys[$candidate_property_name] + && ($class_strings[$candidate_property_name] ?? false); + } else { + $combination->objectlike_class_string_keys[$candidate_property_name] = + ($class_strings[$candidate_property_name] ?? false); + } } if ($type->fallback_params) { @@ -1421,7 +1435,7 @@ final class TypeCombiner } else { $objectlike = new TKeyedArray( $combination->objectlike_entries, - null, + array_filter($combination->objectlike_class_string_keys), $sealed || $fallback_key_type === null || $fallback_value_type === null ? null : [$fallback_key_type, $fallback_value_type], diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 751d76f63..744959d01 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -1148,7 +1148,9 @@ class Reconciler $base_key = implode($key_parts); - $result_type = $result_type->setPossiblyUndefined(count($array_key_offsets) > 1); + $result_type = $result_type->setPossiblyUndefined( + $result_type->possibly_undefined || count($array_key_offsets) > 1, + ); foreach ($array_key_offsets as $array_key_offset) { if (isset($existing_types[$base_key]) && $array_key_offset !== false) { diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index d016298c4..d7bf6e6e5 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -55,6 +55,24 @@ class ArrayAssignmentTest extends TestCase '$resultOpt===' => 'array{a?: true, b?: true}', ], ], + 'assignUnionOfLiteralsClassKeys' => [ + 'code' => ' $v) { + $vv = new $k; + }', + 'assertions' => [ + '$result===' => 'array{a::class: true, b::class: true}', + ], + ], 'genericArrayCreationWithSingleIntValue' => [ 'code' => '