Skip to content

Commit 71cb8ca

Browse files
ondrejmirtesclaude
andcommitted
Keep an empty array next to an oversized array in a union
reduceArrays() leaves empty constant arrays out when it generalizes to an oversized array, but a union of an array that already is oversized with array{} went through processArrayTypes(), which folded array{} into it: the oversized accessory then rejected [] (a false positive for T inferred from both) and non-empty-array accepted the union (a missed error). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VpbB99tJfUmeArX74xqvHv
1 parent 3c01737 commit 71cb8ca

6 files changed

Lines changed: 711 additions & 38 deletions

File tree

‎src/Type/TypeCombinator.php‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,28 @@ private static function processArrayAccessoryTypes(array $arrayTypes): array
10371037
return $commonAccessoryTypes;
10381038
}
10391039

1040+
/**
1041+
* @param Type[] $arrayTypes
1042+
*/
1043+
private static function findEmptyArrayNextToOversizedArray(array $arrayTypes): ?Type
1044+
{
1045+
if (count($arrayTypes) < 2) {
1046+
return null;
1047+
}
1048+
1049+
$emptyArray = null;
1050+
$hasOversizedArray = false;
1051+
foreach ($arrayTypes as $arrayType) {
1052+
if ($arrayType->isOversizedArray()->yes()) {
1053+
$hasOversizedArray = true;
1054+
} elseif ($emptyArray === null && $arrayType->isConstantArray()->yes() && $arrayType->isIterableAtLeastOnce()->no()) {
1055+
$emptyArray = $arrayType;
1056+
}
1057+
}
1058+
1059+
return $hasOversizedArray ? $emptyArray : null;
1060+
}
1061+
10401062
/**
10411063
* @param list<Type> $arrayTypes
10421064
* @return Type[]
@@ -1047,6 +1069,22 @@ private static function processArrayTypes(array $arrayTypes): array
10471069
return [];
10481070
}
10491071

1072+
$emptyArray = self::findEmptyArrayNextToOversizedArray($arrayTypes);
1073+
if ($emptyArray !== null) {
1074+
// like reduceArrays() does when it generalizes to an oversized array:
1075+
// folded into it, the empty array would be lost to the oversized
1076+
// accessory, which accepts only arrays that hold something
1077+
$otherArrayTypes = [];
1078+
foreach ($arrayTypes as $arrayType) {
1079+
if ($arrayType->isConstantArray()->yes() && $arrayType->isIterableAtLeastOnce()->no()) {
1080+
continue;
1081+
}
1082+
$otherArrayTypes[] = $arrayType;
1083+
}
1084+
1085+
return [$emptyArray, ...self::processArrayTypes($otherArrayTypes)];
1086+
}
1087+
10501088
$accessoryTypes = self::processArrayAccessoryTypes($arrayTypes);
10511089

10521090
if (count($arrayTypes) === 1) {
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace OversizedArrayUnionWithEmptyArray;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function doFoo(bool $flag): void
8+
{
9+
$big = [
10+
'K0' => 'v0',
11+
'K1' => 'v1',
12+
'K2' => 'v2',
13+
'K3' => 'v3',
14+
'K4' => 'v4',
15+
'K5' => 'v5',
16+
'K6' => 'v6',
17+
'K7' => 'v7',
18+
'K8' => 'v8',
19+
'K9' => 'v9',
20+
'K10' => 'v10',
21+
'K11' => 'v11',
22+
'K12' => 'v12',
23+
'K13' => 'v13',
24+
'K14' => 'v14',
25+
'K15' => 'v15',
26+
'K16' => 'v16',
27+
'K17' => 'v17',
28+
'K18' => 'v18',
29+
'K19' => 'v19',
30+
'K20' => 'v20',
31+
'K21' => 'v21',
32+
'K22' => 'v22',
33+
'K23' => 'v23',
34+
'K24' => 'v24',
35+
'K25' => 'v25',
36+
'K26' => 'v26',
37+
'K27' => 'v27',
38+
'K28' => 'v28',
39+
'K29' => 'v29',
40+
'K30' => 'v30',
41+
'K31' => 'v31',
42+
'K32' => 'v32',
43+
'K33' => 'v33',
44+
'K34' => 'v34',
45+
'K35' => 'v35',
46+
'K36' => 'v36',
47+
'K37' => 'v37',
48+
'K38' => 'v38',
49+
'K39' => 'v39',
50+
'K40' => 'v40',
51+
'K41' => 'v41',
52+
'K42' => 'v42',
53+
'K43' => 'v43',
54+
'K44' => 'v44',
55+
'K45' => 'v45',
56+
'K46' => 'v46',
57+
'K47' => 'v47',
58+
'K48' => 'v48',
59+
'K49' => 'v49',
60+
'K50' => 'v50',
61+
'K51' => 'v51',
62+
'K52' => 'v52',
63+
'K53' => 'v53',
64+
'K54' => 'v54',
65+
'K55' => 'v55',
66+
'K56' => 'v56',
67+
'K57' => 'v57',
68+
'K58' => 'v58',
69+
'K59' => 'v59',
70+
'K60' => 'v60',
71+
'K61' => 'v61',
72+
'K62' => 'v62',
73+
'K63' => 'v63',
74+
'K64' => 'v64',
75+
'K65' => 'v65',
76+
'K66' => 'v66',
77+
'K67' => 'v67',
78+
'K68' => 'v68',
79+
'K69' => 'v69',
80+
'K70' => 'v70',
81+
'K71' => 'v71',
82+
'K72' => 'v72',
83+
'K73' => 'v73',
84+
'K74' => 'v74',
85+
'K75' => 'v75',
86+
'K76' => 'v76',
87+
'K77' => 'v77',
88+
'K78' => 'v78',
89+
'K79' => 'v79',
90+
'K80' => 'v80',
91+
'K81' => 'v81',
92+
'K82' => 'v82',
93+
'K83' => 'v83',
94+
'K84' => 'v84',
95+
'K85' => 'v85',
96+
'K86' => 'v86',
97+
'K87' => 'v87',
98+
'K88' => 'v88',
99+
'K89' => 'v89',
100+
'K90' => 'v90',
101+
'K91' => 'v91',
102+
'K92' => 'v92',
103+
'K93' => 'v93',
104+
'K94' => 'v94',
105+
'K95' => 'v95',
106+
'K96' => 'v96',
107+
'K97' => 'v97',
108+
'K98' => 'v98',
109+
'K99' => 'v99',
110+
'K100' => 'v100',
111+
'K101' => 'v101',
112+
'K102' => 'v102',
113+
'K103' => 'v103',
114+
'K104' => 'v104',
115+
'K105' => 'v105',
116+
'K106' => 'v106',
117+
'K107' => 'v107',
118+
'K108' => 'v108',
119+
'K109' => 'v109',
120+
'K110' => 'v110',
121+
'K111' => 'v111',
122+
'K112' => 'v112',
123+
'K113' => 'v113',
124+
'K114' => 'v114',
125+
'K115' => 'v115',
126+
'K116' => 'v116',
127+
'K117' => 'v117',
128+
'K118' => 'v118',
129+
'K119' => 'v119',
130+
'K120' => 'v120',
131+
'K121' => 'v121',
132+
'K122' => 'v122',
133+
'K123' => 'v123',
134+
'K124' => 'v124',
135+
'K125' => 'v125',
136+
'K126' => 'v126',
137+
'K127' => 'v127',
138+
'K128' => 'v128',
139+
'K129' => 'v129',
140+
'K130' => 'v130',
141+
'K131' => 'v131',
142+
'K132' => 'v132',
143+
'K133' => 'v133',
144+
'K134' => 'v134',
145+
'K135' => 'v135',
146+
'K136' => 'v136',
147+
'K137' => 'v137',
148+
'K138' => 'v138',
149+
'K139' => 'v139',
150+
'K140' => 'v140',
151+
'K141' => 'v141',
152+
'K142' => 'v142',
153+
'K143' => 'v143',
154+
'K144' => 'v144',
155+
'K145' => 'v145',
156+
'K146' => 'v146',
157+
'K147' => 'v147',
158+
'K148' => 'v148',
159+
'K149' => 'v149',
160+
'K150' => 'v150',
161+
'K151' => 'v151',
162+
'K152' => 'v152',
163+
'K153' => 'v153',
164+
'K154' => 'v154',
165+
'K155' => 'v155',
166+
'K156' => 'v156',
167+
'K157' => 'v157',
168+
'K158' => 'v158',
169+
'K159' => 'v159',
170+
'K160' => 'v160',
171+
'K161' => 'v161',
172+
'K162' => 'v162',
173+
'K163' => 'v163',
174+
'K164' => 'v164',
175+
'K165' => 'v165',
176+
'K166' => 'v166',
177+
'K167' => 'v167',
178+
'K168' => 'v168',
179+
'K169' => 'v169',
180+
'K170' => 'v170',
181+
'K171' => 'v171',
182+
'K172' => 'v172',
183+
'K173' => 'v173',
184+
'K174' => 'v174',
185+
'K175' => 'v175',
186+
'K176' => 'v176',
187+
'K177' => 'v177',
188+
'K178' => 'v178',
189+
'K179' => 'v179',
190+
'K180' => 'v180',
191+
'K181' => 'v181',
192+
'K182' => 'v182',
193+
'K183' => 'v183',
194+
'K184' => 'v184',
195+
'K185' => 'v185',
196+
'K186' => 'v186',
197+
'K187' => 'v187',
198+
'K188' => 'v188',
199+
'K189' => 'v189',
200+
'K190' => 'v190',
201+
'K191' => 'v191',
202+
'K192' => 'v192',
203+
'K193' => 'v193',
204+
'K194' => 'v194',
205+
'K195' => 'v195',
206+
'K196' => 'v196',
207+
'K197' => 'v197',
208+
'K198' => 'v198',
209+
'K199' => 'v199',
210+
'K200' => 'v200',
211+
'K201' => 'v201',
212+
'K202' => 'v202',
213+
'K203' => 'v203',
214+
'K204' => 'v204',
215+
'K205' => 'v205',
216+
'K206' => 'v206',
217+
'K207' => 'v207',
218+
'K208' => 'v208',
219+
'K209' => 'v209',
220+
'K210' => 'v210',
221+
'K211' => 'v211',
222+
'K212' => 'v212',
223+
'K213' => 'v213',
224+
'K214' => 'v214',
225+
'K215' => 'v215',
226+
'K216' => 'v216',
227+
'K217' => 'v217',
228+
'K218' => 'v218',
229+
'K219' => 'v219',
230+
'K220' => 'v220',
231+
'K221' => 'v221',
232+
'K222' => 'v222',
233+
'K223' => 'v223',
234+
'K224' => 'v224',
235+
'K225' => 'v225',
236+
'K226' => 'v226',
237+
'K227' => 'v227',
238+
'K228' => 'v228',
239+
'K229' => 'v229',
240+
'K230' => 'v230',
241+
'K231' => 'v231',
242+
'K232' => 'v232',
243+
'K233' => 'v233',
244+
'K234' => 'v234',
245+
'K235' => 'v235',
246+
'K236' => 'v236',
247+
'K237' => 'v237',
248+
'K238' => 'v238',
249+
'K239' => 'v239',
250+
'K240' => 'v240',
251+
'K241' => 'v241',
252+
'K242' => 'v242',
253+
'K243' => 'v243',
254+
'K244' => 'v244',
255+
'K245' => 'v245',
256+
'K246' => 'v246',
257+
'K247' => 'v247',
258+
'K248' => 'v248',
259+
'K249' => 'v249',
260+
'K250' => 'v250',
261+
'K251' => 'v251',
262+
'K252' => 'v252',
263+
'K253' => 'v253',
264+
'K254' => 'v254',
265+
'K255' => 'v255',
266+
'K256' => 'v256',
267+
'K257' => 'v257',
268+
'K258' => 'v258',
269+
'K259' => 'v259',
270+
];
271+
assertType("array{}|(non-empty-array<literal-string&non-falsy-string&uppercase-string, literal-string&lowercase-string&non-falsy-string>&oversized-array)", $flag ? $big : []);
272+
assertType("array{}|(non-empty-array<literal-string&non-falsy-string&uppercase-string, literal-string&lowercase-string&non-falsy-string>&oversized-array)", $flag ? [] : $big);
273+
}

‎tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3268,4 +3268,15 @@ public function testRoundModePhp84(): void
32683268
]);
32693269
}
32703270

3271+
public function testOversizedArrayUnionWithEmptyArray(): void
3272+
{
3273+
$this->analyse([__DIR__ . '/data/oversized-array-union-with-empty-array.php'], [
3274+
[
3275+
'Parameter #1 $a of function OversizedArrayUnionWithEmptyArrayRule\\nonEmpty expects non-empty-array<string, string>, array{}|(non-empty-array<literal-string&non-falsy-string, literal-string&non-falsy-string>) given.',
3276+
286,
3277+
'array{}|(non-empty-array<literal-string&non-falsy-string, literal-string&non-falsy-string>) might be empty.',
3278+
],
3279+
]);
3280+
}
3281+
32713282
}

0 commit comments

Comments
 (0)