Skip to content

Commit 09fbc92

Browse files
committed
Optimize array_map with many arrays
1 parent 44e40f0 commit 09fbc92

File tree

4 files changed

+285
-16
lines changed

4 files changed

+285
-16
lines changed

src/Type/Php/ArrayMapFunctionReturnTypeExtension.php

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
6666
$constantArrays = $arrayType->getConstantArrays();
6767
if (count($constantArrays) > 0) {
6868
$arrayTypes = [];
69-
foreach ($constantArrays as $constantArray) {
70-
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
71-
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
72-
$returnedArrayBuilder->setOffsetValueType(
73-
$keyType,
74-
$valueType,
75-
$constantArray->isOptionalKey($i),
76-
);
69+
$totalCount = TypeCombinator::countConstantArrayValueTypes($constantArrays) * TypeCombinator::countConstantArrayValueTypes([$valueType]);
70+
if ($totalCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
71+
foreach ($constantArrays as $constantArray) {
72+
$returnedArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
73+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
74+
$returnedArrayBuilder->setOffsetValueType(
75+
$keyType,
76+
$valueType,
77+
$constantArray->isOptionalKey($i),
78+
);
79+
}
80+
$returnedArray = $returnedArrayBuilder->getArray();
81+
if ($constantArray->isList()->yes()) {
82+
$returnedArray = AccessoryArrayListType::intersectWith($returnedArray);
83+
}
84+
$arrayTypes[] = $returnedArray;
7785
}
78-
$returnedArray = $returnedArrayBuilder->getArray();
79-
if ($constantArray->isList()->yes()) {
80-
$returnedArray = AccessoryArrayListType::intersectWith($returnedArray);
81-
}
82-
$arrayTypes[] = $returnedArray;
83-
}
8486

85-
$mappedArrayType = TypeCombinator::union(...$arrayTypes);
87+
$mappedArrayType = TypeCombinator::union(...$arrayTypes);
88+
} else {
89+
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
90+
$arrayType->getIterableKeyType(),
91+
$valueType,
92+
), ...TypeUtils::getAccessoryTypes($arrayType));
93+
}
8694
} elseif ($arrayType->isArray()->yes()) {
8795
$mappedArrayType = TypeCombinator::intersect(new ArrayType(
8896
$arrayType->getIterableKeyType(),

src/Type/TypeCombinator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ private static function optimizeConstantArrays(array $types): array
826826
/**
827827
* @param Type[] $types
828828
*/
829-
private static function countConstantArrayValueTypes(array $types): int
829+
public static function countConstantArrayValueTypes(array $types): int
830830
{
831831
$constantArrayValuesCount = 0;
832832
foreach ($types as $type) {

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,6 +1406,16 @@ public function testBug11292(): void
14061406
$this->assertNoErrors($errors);
14071407
}
14081408

1409+
public function testBug11297(): void
1410+
{
1411+
if (PHP_VERSION_ID < 80100) {
1412+
$this->markTestSkipped('Test requires PHP 8.1.');
1413+
}
1414+
1415+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-11297.php');
1416+
$this->assertNoErrors($errors);
1417+
}
1418+
14091419
/**
14101420
* @param string[]|null $allAnalysedFiles
14111421
* @return Error[]
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug11297;
4+
5+
class ClassA {
6+
/** @param array<mixed> $array */
7+
public static function doSomething(string $string, array $array): void
8+
{
9+
}
10+
}
11+
12+
enum Icon: string
13+
{
14+
case CASE1 = 'case1';
15+
case CASE2 = 'case2';
16+
case CASE3 = 'case3';
17+
case CASE4 = 'case4';
18+
case CASE5 = 'case5';
19+
case CASE6 = 'case6';
20+
case CASE7 = 'case7';
21+
case CASE8 = 'case8';
22+
case CASE9 = 'case9';
23+
case CASE10 = 'case10';
24+
case CASE11 = 'case11';
25+
case CASE12 = 'case12';
26+
case CASE13 = 'case13';
27+
case CASE14 = 'case14';
28+
case CASE15 = 'case15';
29+
case CASE16 = 'case16';
30+
case CASE17 = 'case17';
31+
case CASE18 = 'case18';
32+
case CASE19 = 'case19';
33+
case CASE20 = 'case20';
34+
case CASE21 = 'case21';
35+
case CASE22 = 'case22';
36+
case CASE23 = 'case23';
37+
case CASE24 = 'case24';
38+
case CASE25 = 'case25';
39+
case CASE26 = 'case26';
40+
case CASE27 = 'case27';
41+
case CASE28 = 'case28';
42+
case CASE29 = 'case29';
43+
case CASE30 = 'case30';
44+
case CASE31 = 'case31';
45+
case CASE32 = 'case32';
46+
case CASE33 = 'case33';
47+
case CASE34 = 'case34';
48+
case CASE35 = 'case35';
49+
case CASE36 = 'case36';
50+
case CASE37 = 'case37';
51+
case CASE38 = 'case38';
52+
case CASE39 = 'case39';
53+
case CASE40 = 'case40';
54+
case CASE41 = 'case41';
55+
case CASE42 = 'case42';
56+
case CASE43 = 'case43';
57+
case CASE44 = 'case44';
58+
case CASE45 = 'case45';
59+
case CASE46 = 'case46';
60+
case CASE47 = 'case47';
61+
case CASE48 = 'case48';
62+
case CASE49 = 'case49';
63+
case CASE50 = 'case50';
64+
case CASE51 = 'case51';
65+
case CASE52 = 'case52';
66+
case CASE53 = 'case53';
67+
case CASE54 = 'case54';
68+
case CASE55 = 'case55';
69+
case CASE56 = 'case56';
70+
case CASE57 = 'case57';
71+
case CASE58 = 'case58';
72+
case CASE59 = 'case59';
73+
case CASE60 = 'case60';
74+
case CASE61 = 'case61';
75+
case CASE62 = 'case62';
76+
case CASE63 = 'case63';
77+
case CASE64 = 'case64';
78+
case CASE65 = 'case65';
79+
case CASE66 = 'case66';
80+
case CASE67 = 'case67';
81+
case CASE68 = 'case68';
82+
case CASE69 = 'case69';
83+
case CASE70 = 'case70';
84+
case CASE71 = 'case71';
85+
case CASE72 = 'case72';
86+
case CASE73 = 'case73';
87+
case CASE74 = 'case74';
88+
case CASE75 = 'case75';
89+
case CASE76 = 'case76';
90+
case CASE77 = 'case77';
91+
case CASE78 = 'case78';
92+
case CASE79 = 'case79';
93+
case CASE80 = 'case80';
94+
case CASE81 = 'case81';
95+
case CASE82 = 'case82';
96+
case CASE83 = 'case83';
97+
case CASE84 = 'case84';
98+
case CASE85 = 'case85';
99+
case CASE86 = 'case86';
100+
case CASE87 = 'case87';
101+
case CASE88 = 'case88';
102+
case CASE89 = 'case89';
103+
case CASE90 = 'case90';
104+
case CASE91 = 'case91';
105+
case CASE92 = 'case92';
106+
case CASE93 = 'case93';
107+
case CASE94 = 'case94';
108+
case CASE95 = 'case95';
109+
case CASE96 = 'case96';
110+
case CASE97 = 'case97';
111+
case CASE98 = 'case98';
112+
case CASE99 = 'case99';
113+
case CASE100 = 'case100';
114+
case CASE101 = 'case101';
115+
case CASE102 = 'case102';
116+
case CASE103 = 'case103';
117+
case CASE104 = 'case104';
118+
case CASE105 = 'case105';
119+
case CASE106 = 'case106';
120+
case CASE107 = 'case107';
121+
case CASE108 = 'case108';
122+
case CASE109 = 'case109';
123+
case CASE110 = 'case110';
124+
case CASE111 = 'case111';
125+
case CASE112 = 'case112';
126+
case CASE113 = 'case113';
127+
case CASE114 = 'case114';
128+
case CASE115 = 'case115';
129+
case CASE116 = 'case116';
130+
case CASE117 = 'case117';
131+
case CASE118 = 'case118';
132+
case CASE119 = 'case119';
133+
case CASE120 = 'case120';
134+
case CASE121 = 'case121';
135+
case CASE122 = 'case122';
136+
case CASE123 = 'case123';
137+
case CASE124 = 'case124';
138+
case CASE125 = 'case125';
139+
case CASE126 = 'case126';
140+
case CASE127 = 'case127';
141+
case CASE128 = 'case128';
142+
case CASE129 = 'case129';
143+
case CASE130 = 'case130';
144+
case CASE131 = 'case131';
145+
case CASE132 = 'case132';
146+
case CASE133 = 'case133';
147+
case CASE134 = 'case134';
148+
case CASE135 = 'case135';
149+
case CASE136 = 'case136';
150+
case CASE137 = 'case137';
151+
case CASE138 = 'case138';
152+
case CASE139 = 'case139';
153+
case CASE140 = 'case140';
154+
case CASE141 = 'case141';
155+
case CASE142 = 'case142';
156+
case CASE143 = 'case143';
157+
case CASE144 = 'case144';
158+
case CASE145 = 'case145';
159+
case CASE146 = 'case146';
160+
case CASE147 = 'case147';
161+
case CASE148 = 'case148';
162+
case CASE149 = 'case149';
163+
case CASE150 = 'case150';
164+
case CASE151 = 'case151';
165+
case CASE152 = 'case152';
166+
case CASE153 = 'case153';
167+
case CASE154 = 'case154';
168+
case CASE155 = 'case155';
169+
case CASE156 = 'case156';
170+
case CASE157 = 'case157';
171+
case CASE158 = 'case158';
172+
case CASE159 = 'case159';
173+
case CASE160 = 'case160';
174+
case CASE161 = 'case161';
175+
case CASE162 = 'case162';
176+
case CASE163 = 'case163';
177+
case CASE164 = 'case164';
178+
case CASE165 = 'case165';
179+
case CASE166 = 'case166';
180+
case CASE167 = 'case167';
181+
case CASE168 = 'case168';
182+
case CASE169 = 'case169';
183+
case CASE170 = 'case170';
184+
case CASE171 = 'case171';
185+
case CASE172 = 'case172';
186+
case CASE173 = 'case173';
187+
case CASE174 = 'case174';
188+
case CASE175 = 'case175';
189+
case CASE176 = 'case176';
190+
case CASE177 = 'case177';
191+
case CASE178 = 'case178';
192+
case CASE179 = 'case179';
193+
case CASE180 = 'case180';
194+
case CASE181 = 'case181';
195+
case CASE182 = 'case182';
196+
case CASE183 = 'case183';
197+
case CASE184 = 'case184';
198+
case CASE185 = 'case185';
199+
case CASE186 = 'case186';
200+
case CASE187 = 'case187';
201+
case CASE188 = 'case188';
202+
case CASE189 = 'case189';
203+
case CASE190 = 'case190';
204+
case CASE191 = 'case191';
205+
case CASE192 = 'case192';
206+
case CASE193 = 'case193';
207+
case CASE194 = 'case194';
208+
case CASE195 = 'case195';
209+
case CASE196 = 'case196';
210+
case CASE197 = 'case197';
211+
case CASE198 = 'case198';
212+
case CASE199 = 'case199';
213+
case CASE200 = 'case200';
214+
215+
public function getFileIdentifier(): string
216+
{
217+
return match ($this) {
218+
default => $this->value
219+
};
220+
}
221+
222+
public function getBackendLabelIdentifier(): string
223+
{
224+
return 'foo:' . $this->getLocallangIdentifier();
225+
}
226+
227+
private function getLocallangIdentifier(): string
228+
{
229+
return 'foo.icon.' . $this->value;
230+
}
231+
}
232+
233+
(static function (string $table): void {
234+
/**
235+
* Add TCA for top bar field in pages.
236+
*/
237+
ClassA::doSomething($table, [
238+
'foo' => [
239+
'config' => [
240+
'items' => [['', ''], ...array_map(
241+
static fn (Icon $icon): array => [
242+
$icon->getBackendLabelIdentifier(),
243+
$icon->value,
244+
'foo/' . $icon->getFileIdentifier() . '.svg',
245+
],
246+
Icon::cases(),
247+
)],
248+
],
249+
],
250+
]);
251+
})('foo');

0 commit comments

Comments
 (0)