@@ -18,3 +18,101 @@ function () {
1818 assertType ('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string} ' , $ matches );
1919 }
2020};
21+
22+ class Bug14314Test
23+ {
24+ /**
25+ * Test: union with array{} and arrays with optional keys
26+ * @param array{}|array{0: string, 1: string, 2?: string} $arr
27+ */
28+ public function testOptionalKeysWithEmpty (array $ arr ): void
29+ {
30+ assertType ('array{}|array{0: string, 1: string, 2?: string} ' , $ arr );
31+ if (count ($ arr ) === 3 ) {
32+ assertType ('array{string, string, string} ' , $ arr );
33+ return ;
34+ }
35+ // Fallback keeps full array{0: string, 1: string, 2?: string} since its size
36+ // range (2..3) is not fully contained in sizeType (3)
37+ assertType ('array{}|array{0: string, 1: string, 2?: string} ' , $ arr );
38+ }
39+
40+ /**
41+ * Test: IntegerRange sizeType fully covers optional key size range - array is correctly removed
42+ * @param array{}|array{0: string, 1: string, 2?: string} $arr
43+ * @param int<2, 3> $twoOrThree
44+ */
45+ public function testIntRangeFullyCoveringOptionalKeys (array $ arr , int $ twoOrThree ): void
46+ {
47+ if (count ($ arr ) === $ twoOrThree ) {
48+ assertType ('array{0: string, 1: string, 2?: string} ' , $ arr );
49+ return ;
50+ }
51+ // int<2,3> fully covers the optional-key array's size range (2..3),
52+ // so the array is correctly removed in the falsey branch
53+ assertType ('array{} ' , $ arr );
54+ }
55+
56+ /**
57+ * Test: IntegerRange partially covers optional key size range - array is kept
58+ * @param array{}|array{0: string, 1: string, 2?: string, 3?: string} $arr
59+ * @param int<2, 3> $twoOrThree
60+ */
61+ public function testIntRangePartiallyCoveringOptionalKeys (array $ arr , int $ twoOrThree ): void
62+ {
63+ if (count ($ arr ) === $ twoOrThree ) {
64+ assertType ('array{0: string, 1: string, 2?: string, 3?: string} ' , $ arr );
65+ return ;
66+ }
67+ // int<2,3> does NOT fully cover size range (2..4), so the array is kept
68+ assertType ('array{}|array{0: string, 1: string, 2?: string, 3?: string} ' , $ arr );
69+ }
70+
71+ /**
72+ * Test: IntegerRange sizeType with union of constant arrays including array{}
73+ * @param array{}|array{string}|array{string, string, string, string} $arr
74+ * @param int<2, 4> $twoToFour
75+ */
76+ public function testIntRangeWithUnionAndEmpty (array $ arr , int $ twoToFour ): void
77+ {
78+ if (count ($ arr ) === $ twoToFour ) {
79+ assertType ('array{string, string, string, string} ' , $ arr );
80+ return ;
81+ }
82+ assertType ('array{}|array{string, string, string, string}|array{string} ' , $ arr );
83+ }
84+ }
85+
86+ // Test: sequential count checks preserve narrowing correctly
87+ function () {
88+ preg_match ('/^(.)$/ ' , '' , $ m ) || preg_match ('/^(.)(.)(.)$/ ' , '' , $ m ) || preg_match ('/^(.)(.)(.)(.)(.)(.)$/ ' , '' , $ m );
89+ assertType ('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string} ' , $ m );
90+ if (count ($ m ) === 2 ) {
91+ assertType ('array{non-falsy-string, non-empty-string} ' , $ m );
92+ return ;
93+ }
94+ assertType ('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string} ' , $ m );
95+ if (count ($ m ) === 4 ) {
96+ assertType ('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string} ' , $ m );
97+ return ;
98+ }
99+ assertType ('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string} ' , $ m );
100+ if (count ($ m ) === 7 ) {
101+ assertType ('array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string, non-empty-string} ' , $ m );
102+ }
103+ };
104+
105+ // Test: count narrowing does not lose other variable types
106+ function (int $ x ) {
107+ preg_match ('/^(.)$/ ' , '' , $ matches ) || preg_match ('/^(.)(.)(.)$/ ' , '' , $ matches );
108+ if ($ x > 0 ) {
109+ assertType ('int<1, max> ' , $ x );
110+ if (count ($ matches ) === 2 ) {
111+ assertType ('array{non-falsy-string, non-empty-string} ' , $ matches );
112+ assertType ('int<1, max> ' , $ x );
113+ return ;
114+ }
115+ assertType ('array{}|array{non-falsy-string, non-empty-string, non-empty-string, non-empty-string} ' , $ matches );
116+ assertType ('int<1, max> ' , $ x );
117+ }
118+ };
0 commit comments