Skip to content

Commit 231bfc5

Browse files
staabmondrejmirtes
authored andcommitted
OversizedArrayBuilder: expand unpacked items into types directly instead of feeding synthesized TypeExpr items to the type callback
- OversizedArrayBuilder::build() used to splice unpacked items into new ArrayItem(TypeExpr, TypeExpr) nodes and then resolve them through $getTypeCallback. The callbacks used by ArrayHandler (type callback and collectAbsorbedItems()) only know the literal's own sub-expressions, so an array literal with more than ARRAY_COUNT_LIMIT items and a `...` spread threw ShouldNotHappenException (or hit an undefined index). - The builder now expands every item into a [key type|null, value type] pair and calls the callback only for the literal's real key/value expressions. - The same fix covers every getArrayType() caller: the ArrayHandler type callback, ArrayHandler::collectAbsorbedItems(), the ArgumentsHandler array skeleton for closure arguments, and constant-expression contexts. - Inferred types are unchanged; OversizedArrayBuilder is not turbo-shadowed.
1 parent 4c1d0ec commit 231bfc5

3 files changed

Lines changed: 1399 additions & 36 deletions

File tree

‎src/Type/Constant/OversizedArrayBuilder.php‎

Lines changed: 23 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
namespace PHPStan\Type\Constant;
44

5-
use PhpParser\Node\ArrayItem;
65
use PhpParser\Node\Expr;
76
use PhpParser\Node\Expr\Array_;
87
use PHPStan\DependencyInjection\AutowiredService;
9-
use PHPStan\Node\Expr\TypeExpr;
10-
use PHPStan\ShouldNotHappenException;
118
use PHPStan\Type\Accessory\AccessoryArrayListType;
129
use PHPStan\Type\Accessory\NonEmptyArrayType;
1310
use PHPStan\Type\Accessory\OversizedArrayType;
@@ -17,9 +14,7 @@
1714
use PHPStan\Type\Type;
1815
use PHPStan\Type\TypeCombinator;
1916
use PHPStan\Type\VerbosityLevel;
20-
use function array_splice;
2117
use function array_values;
22-
use function count;
2318
use const PHP_INT_MAX;
2419

2520
#[AutowiredService]
@@ -35,41 +30,34 @@ public function build(Array_ $expr, callable $getTypeCallback): Type
3530
$valueTypes = [];
3631
$keyTypes = [];
3732
$nextAutoIndex = 0;
38-
$items = $expr->items;
39-
for ($i = 0; $i < count($items); $i++) {
40-
$item = $items[$i];
41-
if (!$item->unpack) {
42-
continue;
43-
}
4433

45-
$valueType = $getTypeCallback($item->value);
46-
if ($valueType instanceof ConstantArrayType) {
47-
array_splice($items, $i, 1);
48-
foreach ($valueType->getKeyTypes() as $j => $innerKeyType) {
49-
$innerValueType = $valueType->getValueTypes()[$j];
50-
if ($innerKeyType->isString()->no()) {
51-
$keyExpr = null;
52-
} else {
53-
$keyExpr = new TypeExpr($innerKeyType);
34+
// unpacked items are expanded into [key type or null for an implicit key, value type]
35+
// pairs right here - the callback only knows the literal's own sub-expressions
36+
$entries = [];
37+
foreach ($expr->items as $item) {
38+
if ($item->unpack) {
39+
$unpackedType = $getTypeCallback($item->value);
40+
if ($unpackedType instanceof ConstantArrayType) {
41+
foreach ($unpackedType->getKeyTypes() as $j => $innerKeyType) {
42+
$entries[] = [
43+
$innerKeyType->isString()->no() ? null : $innerKeyType,
44+
$unpackedType->getValueTypes()[$j],
45+
];
5446
}
55-
array_splice($items, $i++, 0, [new ArrayItem(
56-
new TypeExpr($innerValueType),
57-
$keyExpr,
58-
)]);
47+
} else {
48+
$entries[] = [$unpackedType->getIterableKeyType(), $unpackedType->getIterableValueType()];
5949
}
60-
} else {
61-
array_splice($items, $i, 1, [new ArrayItem(
62-
new TypeExpr($valueType->getIterableValueType()),
63-
new TypeExpr($valueType->getIterableKeyType()),
64-
)]);
50+
continue;
6551
}
52+
53+
$entries[] = [
54+
$item->key !== null ? $getTypeCallback($item->key) : null,
55+
$getTypeCallback($item->value),
56+
];
6657
}
67-
foreach ($items as $item) {
68-
if ($item->unpack) {
69-
throw new ShouldNotHappenException();
70-
}
71-
if ($item->key !== null) {
72-
$itemKeyType = $getTypeCallback($item->key);
58+
59+
foreach ($entries as [$itemKeyType, $itemValueType]) {
60+
if ($itemKeyType !== null) {
7361
if (!$itemKeyType instanceof ConstantIntegerType) {
7462
$isList = false;
7563
} elseif ($itemKeyType->getValue() !== $nextAutoIndex) {
@@ -89,7 +77,6 @@ public function build(Array_ $expr, callable $getTypeCallback): Type
8977
$generalizedKeyType = $itemKeyType->generalize(GeneralizePrecision::moreSpecific());
9078
$keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;
9179

92-
$itemValueType = $getTypeCallback($item->value);
9380
$generalizedValueType = $itemValueType->generalize(GeneralizePrecision::moreSpecific());
9481
$valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
9582
}
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15510Closures;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template T
9+
* @param array<string, callable(int): T> $callbacks
10+
* @return T
11+
*/
12+
function call(array $callbacks)
13+
{
14+
return $callbacks['k0'](1);
15+
}
16+
17+
/**
18+
* @param array<string, callable(int): int> $more
19+
*/
20+
function doFoo(array $more): void
21+
{
22+
assertType('int', call([
23+
'k0' => static fn (int $i): int => $i,
24+
'k1' => static fn (int $i): int => $i,
25+
'k2' => static fn (int $i): int => $i,
26+
'k3' => static fn (int $i): int => $i,
27+
'k4' => static fn (int $i): int => $i,
28+
'k5' => static fn (int $i): int => $i,
29+
'k6' => static fn (int $i): int => $i,
30+
'k7' => static fn (int $i): int => $i,
31+
'k8' => static fn (int $i): int => $i,
32+
'k9' => static fn (int $i): int => $i,
33+
'k10' => static fn (int $i): int => $i,
34+
'k11' => static fn (int $i): int => $i,
35+
'k12' => static fn (int $i): int => $i,
36+
'k13' => static fn (int $i): int => $i,
37+
'k14' => static fn (int $i): int => $i,
38+
'k15' => static fn (int $i): int => $i,
39+
'k16' => static fn (int $i): int => $i,
40+
'k17' => static fn (int $i): int => $i,
41+
'k18' => static fn (int $i): int => $i,
42+
'k19' => static fn (int $i): int => $i,
43+
'k20' => static fn (int $i): int => $i,
44+
'k21' => static fn (int $i): int => $i,
45+
'k22' => static fn (int $i): int => $i,
46+
'k23' => static fn (int $i): int => $i,
47+
'k24' => static fn (int $i): int => $i,
48+
'k25' => static fn (int $i): int => $i,
49+
'k26' => static fn (int $i): int => $i,
50+
'k27' => static fn (int $i): int => $i,
51+
'k28' => static fn (int $i): int => $i,
52+
'k29' => static fn (int $i): int => $i,
53+
'k30' => static fn (int $i): int => $i,
54+
'k31' => static fn (int $i): int => $i,
55+
'k32' => static fn (int $i): int => $i,
56+
'k33' => static fn (int $i): int => $i,
57+
'k34' => static fn (int $i): int => $i,
58+
'k35' => static fn (int $i): int => $i,
59+
'k36' => static fn (int $i): int => $i,
60+
'k37' => static fn (int $i): int => $i,
61+
'k38' => static fn (int $i): int => $i,
62+
'k39' => static fn (int $i): int => $i,
63+
'k40' => static fn (int $i): int => $i,
64+
'k41' => static fn (int $i): int => $i,
65+
'k42' => static fn (int $i): int => $i,
66+
'k43' => static fn (int $i): int => $i,
67+
'k44' => static fn (int $i): int => $i,
68+
'k45' => static fn (int $i): int => $i,
69+
'k46' => static fn (int $i): int => $i,
70+
'k47' => static fn (int $i): int => $i,
71+
'k48' => static fn (int $i): int => $i,
72+
'k49' => static fn (int $i): int => $i,
73+
'k50' => static fn (int $i): int => $i,
74+
'k51' => static fn (int $i): int => $i,
75+
'k52' => static fn (int $i): int => $i,
76+
'k53' => static fn (int $i): int => $i,
77+
'k54' => static fn (int $i): int => $i,
78+
'k55' => static fn (int $i): int => $i,
79+
'k56' => static fn (int $i): int => $i,
80+
'k57' => static fn (int $i): int => $i,
81+
'k58' => static fn (int $i): int => $i,
82+
'k59' => static fn (int $i): int => $i,
83+
'k60' => static fn (int $i): int => $i,
84+
'k61' => static fn (int $i): int => $i,
85+
'k62' => static fn (int $i): int => $i,
86+
'k63' => static fn (int $i): int => $i,
87+
'k64' => static fn (int $i): int => $i,
88+
'k65' => static fn (int $i): int => $i,
89+
'k66' => static fn (int $i): int => $i,
90+
'k67' => static fn (int $i): int => $i,
91+
'k68' => static fn (int $i): int => $i,
92+
'k69' => static fn (int $i): int => $i,
93+
'k70' => static fn (int $i): int => $i,
94+
'k71' => static fn (int $i): int => $i,
95+
'k72' => static fn (int $i): int => $i,
96+
'k73' => static fn (int $i): int => $i,
97+
'k74' => static fn (int $i): int => $i,
98+
'k75' => static fn (int $i): int => $i,
99+
'k76' => static fn (int $i): int => $i,
100+
'k77' => static fn (int $i): int => $i,
101+
'k78' => static fn (int $i): int => $i,
102+
'k79' => static fn (int $i): int => $i,
103+
'k80' => static fn (int $i): int => $i,
104+
'k81' => static fn (int $i): int => $i,
105+
'k82' => static fn (int $i): int => $i,
106+
'k83' => static fn (int $i): int => $i,
107+
'k84' => static fn (int $i): int => $i,
108+
'k85' => static fn (int $i): int => $i,
109+
'k86' => static fn (int $i): int => $i,
110+
'k87' => static fn (int $i): int => $i,
111+
'k88' => static fn (int $i): int => $i,
112+
'k89' => static fn (int $i): int => $i,
113+
'k90' => static fn (int $i): int => $i,
114+
'k91' => static fn (int $i): int => $i,
115+
'k92' => static fn (int $i): int => $i,
116+
'k93' => static fn (int $i): int => $i,
117+
'k94' => static fn (int $i): int => $i,
118+
'k95' => static fn (int $i): int => $i,
119+
'k96' => static fn (int $i): int => $i,
120+
'k97' => static fn (int $i): int => $i,
121+
'k98' => static fn (int $i): int => $i,
122+
'k99' => static fn (int $i): int => $i,
123+
'k100' => static fn (int $i): int => $i,
124+
'k101' => static fn (int $i): int => $i,
125+
'k102' => static fn (int $i): int => $i,
126+
'k103' => static fn (int $i): int => $i,
127+
'k104' => static fn (int $i): int => $i,
128+
'k105' => static fn (int $i): int => $i,
129+
'k106' => static fn (int $i): int => $i,
130+
'k107' => static fn (int $i): int => $i,
131+
'k108' => static fn (int $i): int => $i,
132+
'k109' => static fn (int $i): int => $i,
133+
'k110' => static fn (int $i): int => $i,
134+
'k111' => static fn (int $i): int => $i,
135+
'k112' => static fn (int $i): int => $i,
136+
'k113' => static fn (int $i): int => $i,
137+
'k114' => static fn (int $i): int => $i,
138+
'k115' => static fn (int $i): int => $i,
139+
'k116' => static fn (int $i): int => $i,
140+
'k117' => static fn (int $i): int => $i,
141+
'k118' => static fn (int $i): int => $i,
142+
'k119' => static fn (int $i): int => $i,
143+
'k120' => static fn (int $i): int => $i,
144+
'k121' => static fn (int $i): int => $i,
145+
'k122' => static fn (int $i): int => $i,
146+
'k123' => static fn (int $i): int => $i,
147+
'k124' => static fn (int $i): int => $i,
148+
'k125' => static fn (int $i): int => $i,
149+
'k126' => static fn (int $i): int => $i,
150+
'k127' => static fn (int $i): int => $i,
151+
'k128' => static fn (int $i): int => $i,
152+
'k129' => static fn (int $i): int => $i,
153+
'k130' => static fn (int $i): int => $i,
154+
'k131' => static fn (int $i): int => $i,
155+
'k132' => static fn (int $i): int => $i,
156+
'k133' => static fn (int $i): int => $i,
157+
'k134' => static fn (int $i): int => $i,
158+
'k135' => static fn (int $i): int => $i,
159+
'k136' => static fn (int $i): int => $i,
160+
'k137' => static fn (int $i): int => $i,
161+
'k138' => static fn (int $i): int => $i,
162+
'k139' => static fn (int $i): int => $i,
163+
'k140' => static fn (int $i): int => $i,
164+
'k141' => static fn (int $i): int => $i,
165+
'k142' => static fn (int $i): int => $i,
166+
'k143' => static fn (int $i): int => $i,
167+
'k144' => static fn (int $i): int => $i,
168+
'k145' => static fn (int $i): int => $i,
169+
'k146' => static fn (int $i): int => $i,
170+
'k147' => static fn (int $i): int => $i,
171+
'k148' => static fn (int $i): int => $i,
172+
'k149' => static fn (int $i): int => $i,
173+
'k150' => static fn (int $i): int => $i,
174+
'k151' => static fn (int $i): int => $i,
175+
'k152' => static fn (int $i): int => $i,
176+
'k153' => static fn (int $i): int => $i,
177+
'k154' => static fn (int $i): int => $i,
178+
'k155' => static fn (int $i): int => $i,
179+
'k156' => static fn (int $i): int => $i,
180+
'k157' => static fn (int $i): int => $i,
181+
'k158' => static fn (int $i): int => $i,
182+
'k159' => static fn (int $i): int => $i,
183+
'k160' => static fn (int $i): int => $i,
184+
'k161' => static fn (int $i): int => $i,
185+
'k162' => static fn (int $i): int => $i,
186+
'k163' => static fn (int $i): int => $i,
187+
'k164' => static fn (int $i): int => $i,
188+
'k165' => static fn (int $i): int => $i,
189+
'k166' => static fn (int $i): int => $i,
190+
'k167' => static fn (int $i): int => $i,
191+
'k168' => static fn (int $i): int => $i,
192+
'k169' => static fn (int $i): int => $i,
193+
'k170' => static fn (int $i): int => $i,
194+
'k171' => static fn (int $i): int => $i,
195+
'k172' => static fn (int $i): int => $i,
196+
'k173' => static fn (int $i): int => $i,
197+
'k174' => static fn (int $i): int => $i,
198+
'k175' => static fn (int $i): int => $i,
199+
'k176' => static fn (int $i): int => $i,
200+
'k177' => static fn (int $i): int => $i,
201+
'k178' => static fn (int $i): int => $i,
202+
'k179' => static fn (int $i): int => $i,
203+
'k180' => static fn (int $i): int => $i,
204+
'k181' => static fn (int $i): int => $i,
205+
'k182' => static fn (int $i): int => $i,
206+
'k183' => static fn (int $i): int => $i,
207+
'k184' => static fn (int $i): int => $i,
208+
'k185' => static fn (int $i): int => $i,
209+
'k186' => static fn (int $i): int => $i,
210+
'k187' => static fn (int $i): int => $i,
211+
'k188' => static fn (int $i): int => $i,
212+
'k189' => static fn (int $i): int => $i,
213+
'k190' => static fn (int $i): int => $i,
214+
'k191' => static fn (int $i): int => $i,
215+
'k192' => static fn (int $i): int => $i,
216+
'k193' => static fn (int $i): int => $i,
217+
'k194' => static fn (int $i): int => $i,
218+
'k195' => static fn (int $i): int => $i,
219+
'k196' => static fn (int $i): int => $i,
220+
'k197' => static fn (int $i): int => $i,
221+
'k198' => static fn (int $i): int => $i,
222+
'k199' => static fn (int $i): int => $i,
223+
'k200' => static fn (int $i): int => $i,
224+
'k201' => static fn (int $i): int => $i,
225+
'k202' => static fn (int $i): int => $i,
226+
'k203' => static fn (int $i): int => $i,
227+
'k204' => static fn (int $i): int => $i,
228+
'k205' => static fn (int $i): int => $i,
229+
'k206' => static fn (int $i): int => $i,
230+
'k207' => static fn (int $i): int => $i,
231+
'k208' => static fn (int $i): int => $i,
232+
'k209' => static fn (int $i): int => $i,
233+
'k210' => static fn (int $i): int => $i,
234+
'k211' => static fn (int $i): int => $i,
235+
'k212' => static fn (int $i): int => $i,
236+
'k213' => static fn (int $i): int => $i,
237+
'k214' => static fn (int $i): int => $i,
238+
'k215' => static fn (int $i): int => $i,
239+
'k216' => static fn (int $i): int => $i,
240+
'k217' => static fn (int $i): int => $i,
241+
'k218' => static fn (int $i): int => $i,
242+
'k219' => static fn (int $i): int => $i,
243+
'k220' => static fn (int $i): int => $i,
244+
'k221' => static fn (int $i): int => $i,
245+
'k222' => static fn (int $i): int => $i,
246+
'k223' => static fn (int $i): int => $i,
247+
'k224' => static fn (int $i): int => $i,
248+
'k225' => static fn (int $i): int => $i,
249+
'k226' => static fn (int $i): int => $i,
250+
'k227' => static fn (int $i): int => $i,
251+
'k228' => static fn (int $i): int => $i,
252+
'k229' => static fn (int $i): int => $i,
253+
'k230' => static fn (int $i): int => $i,
254+
'k231' => static fn (int $i): int => $i,
255+
'k232' => static fn (int $i): int => $i,
256+
'k233' => static fn (int $i): int => $i,
257+
'k234' => static fn (int $i): int => $i,
258+
'k235' => static fn (int $i): int => $i,
259+
'k236' => static fn (int $i): int => $i,
260+
'k237' => static fn (int $i): int => $i,
261+
'k238' => static fn (int $i): int => $i,
262+
'k239' => static fn (int $i): int => $i,
263+
'k240' => static fn (int $i): int => $i,
264+
'k241' => static fn (int $i): int => $i,
265+
'k242' => static fn (int $i): int => $i,
266+
'k243' => static fn (int $i): int => $i,
267+
'k244' => static fn (int $i): int => $i,
268+
'k245' => static fn (int $i): int => $i,
269+
'k246' => static fn (int $i): int => $i,
270+
'k247' => static fn (int $i): int => $i,
271+
'k248' => static fn (int $i): int => $i,
272+
'k249' => static fn (int $i): int => $i,
273+
'k250' => static fn (int $i): int => $i,
274+
'k251' => static fn (int $i): int => $i,
275+
'k252' => static fn (int $i): int => $i,
276+
'k253' => static fn (int $i): int => $i,
277+
'k254' => static fn (int $i): int => $i,
278+
'k255' => static fn (int $i): int => $i,
279+
'k256' => static fn (int $i): int => $i,
280+
'k257' => static fn (int $i): int => $i,
281+
'k258' => static fn (int $i): int => $i,
282+
'k259' => static fn (int $i): int => $i,
283+
...$more,
284+
...['x' => static fn (int $i): int => $i],
285+
]));
286+
}

0 commit comments

Comments
 (0)