Skip to content

Commit d44ae55

Browse files
authored
Fix type inference of array_sum()
1 parent 9f93828 commit d44ae55

File tree

2 files changed

+244
-17
lines changed

2 files changed

+244
-17
lines changed

src/Type/Php/ArraySumFunctionDynamicReturnTypeExtension.php

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
namespace PHPStan\Type\Php;
44

5+
use PhpParser\Node\Expr\BinaryOp\Mul;
6+
use PhpParser\Node\Expr\BinaryOp\Plus;
57
use PhpParser\Node\Expr\FuncCall;
8+
use PhpParser\Node\Scalar\LNumber;
69
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\Expr\TypeExpr;
711
use PHPStan\Reflection\FunctionReflection;
812
use PHPStan\Reflection\ParametersAcceptorSelector;
913
use PHPStan\Type\Constant\ConstantIntegerType;
1014
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
11-
use PHPStan\Type\FloatType;
12-
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\IntegerRangeType;
1316
use PHPStan\Type\Type;
1417
use PHPStan\Type\TypeCombinator;
15-
use PHPStan\Type\UnionType;
18+
use function count;
1619

1720
final class ArraySumFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
1821
{
@@ -28,28 +31,36 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
2831
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
2932
}
3033

31-
$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
32-
$itemType = $arrayType->getIterableValueType();
34+
$argType = $scope->getType($functionCall->getArgs()[0]->value);
35+
$resultTypes = [];
3336

34-
if ($arrayType->isIterableAtLeastOnce()->no()) {
35-
return new ConstantIntegerType(0);
36-
}
37+
if (count($argType->getConstantArrays()) > 0) {
38+
foreach ($argType->getConstantArrays() as $constantArray) {
39+
$node = new LNumber(0);
3740

38-
$intUnionFloat = new UnionType([new IntegerType(), new FloatType()]);
41+
foreach ($constantArray->getValueTypes() as $i => $type) {
42+
if ($constantArray->isOptionalKey($i)) {
43+
$node = new Plus($node, new TypeExpr(TypeCombinator::union($type, new ConstantIntegerType(0))));
44+
} else {
45+
$node = new Plus($node, new TypeExpr($type));
46+
}
47+
}
3948

40-
if ($arrayType->isIterableAtLeastOnce()->yes()) {
41-
if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) {
42-
return $itemType;
49+
$resultTypes[] = $scope->getType($node);
4350
}
51+
} else {
52+
$itemType = $argType->getIterableValueType();
53+
54+
$mulNode = new Mul(new TypeExpr($itemType), new TypeExpr(IntegerRangeType::fromInterval(0, null)));
4455

45-
return $intUnionFloat;
56+
$resultTypes[] = $scope->getType(new Plus(new TypeExpr($itemType), $mulNode));
4657
}
4758

48-
if ($intUnionFloat->isSuperTypeOf($itemType)->yes()) {
49-
return TypeCombinator::union(new ConstantIntegerType(0), $itemType);
59+
if (!$argType->isIterableAtLeastOnce()->yes()) {
60+
$resultTypes[] = new ConstantIntegerType(0);
5061
}
5162

52-
return TypeCombinator::union(new ConstantIntegerType(0), $intUnionFloat);
63+
return TypeCombinator::union(...$resultTypes)->toNumber();
5364
}
5465

5566
}

tests/PHPStan/Analyser/data/array-sum.php

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function foo3($floatList)
3737
function foo4($list)
3838
{
3939
$sum = array_sum($list);
40-
assertType('float|int', $sum);
40+
assertType('(float|int)', $sum);
4141
}
4242

4343
/**
@@ -48,3 +48,219 @@ function foo5($list)
4848
$sum = array_sum($list);
4949
assertType('float|int', $sum);
5050
}
51+
52+
/**
53+
* @param list<0> $list
54+
*/
55+
function foo6($list)
56+
{
57+
assertType('0', array_sum($list));
58+
}
59+
/**
60+
* @param list<1> $list
61+
*/
62+
function foo7($list)
63+
{
64+
assertType('int<0, max>', array_sum($list));
65+
}
66+
67+
/**
68+
* @param non-empty-list<1> $list
69+
*/
70+
function foo8($list)
71+
{
72+
assertType('int<1, max>', array_sum($list));
73+
}
74+
75+
/**
76+
* @param list<-1> $list
77+
*/
78+
function foo9($list)
79+
{
80+
assertType('int<min, 0>', array_sum($list));
81+
}
82+
83+
/**
84+
* @param list<1|2|3> $list
85+
*/
86+
function foo10($list)
87+
{
88+
assertType('int<0, max>', array_sum($list));
89+
}
90+
91+
/**
92+
* @param non-empty-list<1|2|3> $list
93+
*/
94+
function foo11($list)
95+
{
96+
assertType('int<1, max>', array_sum($list));
97+
}
98+
99+
/**
100+
* @param list<1|-1> $list
101+
*/
102+
function foo12($list)
103+
{
104+
assertType('int', array_sum($list));
105+
}
106+
/**
107+
* @param non-empty-list<1|-1> $list
108+
*/
109+
function foo13($list)
110+
{
111+
assertType('int', array_sum($list));
112+
}
113+
114+
/**
115+
* @param array{0} $list
116+
*/
117+
function foo14($list)
118+
{
119+
assertType('0', array_sum($list));
120+
}
121+
/**
122+
* @param array{1} $list
123+
*/
124+
function foo15($list)
125+
{
126+
assertType('1', array_sum($list));
127+
}
128+
129+
/**
130+
* @param array{1, 2, 3} $list
131+
*/
132+
function foo16($list)
133+
{
134+
assertType('6', array_sum($list));
135+
}
136+
137+
/**
138+
* @param array{1, int} $list
139+
*/
140+
function foo17($list)
141+
{
142+
assertType('int', array_sum($list));
143+
}
144+
145+
/**
146+
* @param array{1, float} $list
147+
*/
148+
function foo18($list)
149+
{
150+
assertType('float', array_sum($list));
151+
}
152+
153+
/**
154+
* @param array{} $list
155+
*/
156+
function foo19($list)
157+
{
158+
assertType('0', array_sum($list));
159+
}
160+
161+
162+
/**
163+
* @param list<1|float> $list
164+
*/
165+
function foo20($list)
166+
{
167+
assertType('float|int<0, max>', array_sum($list));
168+
}
169+
170+
/**
171+
* @param array{1, int|float} $list
172+
*/
173+
function foo21($list)
174+
{
175+
assertType('float|int', array_sum($list));
176+
}
177+
178+
/**
179+
* @param array{1, string} $list
180+
*/
181+
function foo22($list)
182+
{
183+
assertType('float|int', array_sum($list));
184+
}
185+
186+
187+
/**
188+
* @param array{1, 3.2} $list
189+
*/
190+
function foo23($list)
191+
{
192+
assertType('4.2', array_sum($list));
193+
}
194+
195+
/**
196+
* @param array{1, float|4} $list
197+
*/
198+
function foo24($list)
199+
{
200+
assertType('5|float', array_sum($list));
201+
}
202+
203+
/**
204+
* @param array{1, 2|3.4} $list
205+
*/
206+
function foo25($list)
207+
{
208+
assertType('3|4.4', array_sum($list));
209+
}
210+
211+
/**
212+
* @param array{1, 2.4|3.4} $list
213+
*/
214+
function foo26($list)
215+
{
216+
assertType('3.4|4.4', array_sum($list));
217+
}
218+
219+
220+
/**
221+
* @param array{1}|array{2, 3} $list
222+
*/
223+
function foo27($list)
224+
{
225+
assertType('1|5', array_sum($list));
226+
}
227+
228+
/**
229+
* @param array{1}|list<1>|array{float} $list
230+
*/
231+
function foo28($list)
232+
{
233+
assertType('float|int<0, max>', array_sum($list));
234+
}
235+
236+
/**
237+
* @param non-empty-list<int<min, -1>|int<1, max>> $list
238+
*/
239+
function foo29($list)
240+
{
241+
assertType('int', array_sum($list));
242+
}
243+
244+
/**
245+
* @param array{'133', 3} $list
246+
*/
247+
function foo30($list)
248+
{
249+
assertType('136', array_sum($list));
250+
}
251+
252+
/**
253+
* @param array{0: 1, 1?: 2, 2?: 3} $list
254+
*/
255+
function foo31($list)
256+
{
257+
assertType('1|3|4|6', array_sum($list));
258+
}
259+
260+
/**
261+
* @param mixed $list
262+
*/
263+
function foo32($list)
264+
{
265+
assertType('(float|int)', array_sum($list));
266+
}

0 commit comments

Comments
 (0)