@@ -63,6 +63,12 @@ public static function nonEmptyProvider(): iterable
6363 yield 'nonempty intersection survives integer-key reindexing ' => [
6464 'array-merge<array{2?: 1, 7?: 2}&non-empty-array> ' ,
6565 ];
66+ yield 'nonempty generic operand before a possibly empty operand ' => [
67+ 'array-merge<non-empty-array<string, int>, array<string, int>> ' ,
68+ ];
69+ yield 'nonempty generic operand after a possibly empty operand ' => [
70+ 'array-merge<array<string, int>, non-empty-array<string, int>> ' ,
71+ ];
6672 }
6773
6874 #[DataProvider('nonEmptyProvider ' )]
@@ -92,6 +98,9 @@ public static function possiblyEmptyProvider(): iterable
9298 yield 'possible empty survivor beside an impossible branch ' => [
9399 'array-merge<array{}|array{bad: never}|array{a: 1}> ' ,
94100 ];
101+ yield 'serialized benevolent union with conflicting key orders ' => [
102+ 'array-merge<__array_merge_benevolent<array{}|array{a: 1, b: 2}|array{b: 2, a: 1}>> ' ,
103+ ];
95104 }
96105
97106 #[DataProvider('possiblyEmptyProvider ' )]
@@ -201,6 +210,57 @@ public function testBenevolentUnionRetainsReachableOptionalShapeAlternative(): v
201210 $ this ->assertTrue ($ result ->isIterableAtLeastOnce ()->maybe (), $ description );
202211 }
203212
213+ public function testBenevolentUnionRetainsEmptyThroughKeyOrderFallback (): void
214+ {
215+ $ empty = self ::constantArrayFromRuntime ([]);
216+ $ firstOrder = self ::constantArrayFromRuntime (['a ' => 1 , 'b ' => 2 ]);
217+ $ secondOrder = self ::constantArrayFromRuntime (['b ' => 2 , 'a ' => 1 ]);
218+ $ operand = new BenevolentUnionType ([$ empty , $ firstOrder , $ secondOrder ]);
219+ $ result = (new ArrayMergeType ([$ operand ]))->resolve ();
220+ $ description = $ result ->describe (VerbosityLevel::precise ());
221+
222+ foreach ([$ result , $ result ->getKeysArray (), $ result ->getValuesArray ()] as $ type ) {
223+ $ this ->assertTrue ($ type ->isSuperTypeOf ($ empty )->yes (), $ description );
224+ $ this ->assertTrue ($ type ->isIterableAtLeastOnce ()->maybe (), $ description );
225+ }
226+
227+ $ this ->assertTrue ($ result ->isSuperTypeOf ($ firstOrder )->yes (), $ description );
228+ $ this ->assertTrue ($ result ->isSuperTypeOf ($ secondOrder )->yes (), $ description );
229+ }
230+
231+ public function testBenevolentUnionRetainsOptionalShapeThroughKeyOrderFallback (): void
232+ {
233+ $ empty = self ::constantArrayFromRuntime ([]);
234+ $ operand = new BenevolentUnionType ([
235+ self ::optionalStringShape (),
236+ self ::constantArrayFromRuntime (['a ' => 1 , 'b ' => 2 ]),
237+ self ::constantArrayFromRuntime (['b ' => 2 , 'a ' => 1 ]),
238+ ]);
239+ $ result = (new ArrayMergeType ([$ operand ]))->resolve ();
240+ $ description = $ result ->describe (VerbosityLevel::precise ());
241+
242+ foreach ([$ result , $ result ->getKeysArray (), $ result ->getValuesArray ()] as $ type ) {
243+ $ this ->assertTrue ($ type ->isSuperTypeOf ($ empty )->yes (), $ description );
244+ $ this ->assertTrue ($ type ->isIterableAtLeastOnce ()->maybe (), $ description );
245+ }
246+ }
247+
248+ public function testBenevolentUnionOfNonEmptyGenericArraysRemainsNonEmpty (): void
249+ {
250+ $ resolver = self ::getContainer ()->getByType (TypeStringResolver::class);
251+ $ operand = new BenevolentUnionType ([
252+ $ resolver ->resolve ('non-empty-array<string, int> ' ),
253+ $ resolver ->resolve ('non-empty-array<int, int> ' ),
254+ ]);
255+ $ result = (new ArrayMergeType ([$ operand ]))->resolve ();
256+ $ description = $ result ->describe (VerbosityLevel::precise ());
257+
258+ $ this ->assertTrue ($ result ->isIterableAtLeastOnce ()->yes (), $ description );
259+ $ this ->assertTrue ($ result ->isSuperTypeOf (self ::constantArrayFromRuntime ([]))->no (), $ description );
260+ $ this ->assertTrue ($ result ->isSuperTypeOf (self ::constantArrayFromRuntime (['a ' => 1 ]))->yes (), $ description );
261+ $ this ->assertTrue ($ result ->isSuperTypeOf (self ::constantArrayFromRuntime ([2 ]))->yes (), $ description );
262+ }
263+
204264 public function testSerializedBenevolentUnionRetainsReachableEmptyAlternative (): void
205265 {
206266 $ merge = self ::getContainer ()->getByType (TypeStringResolver::class)->resolve (
0 commit comments