diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 0132e4e6bf4..89abc13b52c 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -676,7 +676,11 @@ private static function scrapeTypeProperties( if (!$candidate_property_type->possibly_undefined) { $has_defined_keys = true; - } elseif ($combination->fallbackKeyContains($candidate_property_name)) { + } + + if (($candidate_property_type->possibly_undefined || ($value_type->possibly_undefined ?? true)) + && $combination->fallbackKeyContains($candidate_property_name) + ) { $combination->objectlike_entries[$candidate_property_name] = Type::combineUnionTypes( $combination->objectlike_entries[$candidate_property_name], $combination->objectlike_value_type, diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 071b4b8d617..f116e3703a9 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -7,6 +7,8 @@ use Psalm\Type; use Psalm\Type\Atomic; +use function array_reverse; + class TypeCombinationTest extends TestCase { use ValidCodeAnalysisTestTrait; @@ -30,6 +32,11 @@ public function testValidTypeCombination(string $expected, array $types): void $expected, TypeCombiner::combine($converted_types)->getId(), ); + + $this->assertSame( + $expected, + TypeCombiner::combine(array_reverse($converted_types))->getId(), + ); } public function providerValidCodeParse(): iterable @@ -90,6 +97,20 @@ function expectsTraversableOrArray($_a): void public function providerTestValidTypeCombination(): array { return [ + 'complexArrayFallback1' => [ + 'array{other_references: list|null, taint_trace: list>|null, ...}', + [ + 'array{other_references: list|null, taint_trace: null}&array', + 'array{other_references: list|null, taint_trace: list>}&array', + ], + ], + 'complexArrayFallback2' => [ + 'list{0?: 0|a, 1?: 0|a, ..., a>}', + [ + 'list', + 'list{0, 0}', + ], + ], 'intOrString' => [ 'int|string', [