Skip to content

Commit b13462d

Browse files
phpstan-botclaude
authored andcommitted
Treat a template type of mixed operand like mixed in arithmetic so the result stays a benevolent (float|int)
- InitializerExprTypeResolver::resolveCommonMath() decided whether an operand is mixed via `TypeCombinator::union($left, $right) instanceof MixedType`. A TemplateMixedType does not absorb the other operand in a union (`T|int`), so `T + int` produced a strict `float|int` while `mixed + int` and `T + U` produced a benevolent `(float|int)`. The check now also looks at each operand directly. Affects +, -, *, /, <<, >> and their compound assignments / ++ / --. - In 2.3 the array_reduce() callback is typed from the callable(TReturn, TIn) parameter, so `$carry` is TReturn while gathering the closure type; `$carry + map($val)` then resolved TReturn to strict float|int, which the `int` return type rejected. - Mirrored the change in turbo-ext/src/InitializerExprTypeResolver.cpp. - Probed unary -/+ (strict float|int for both mixed and T, consistent), ** and % (already identical for mixed and T), and the `T + U` / array `+` paths (already go through `instanceof MixedType` on each operand). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4ef9ab7 commit b13462d

5 files changed

Lines changed: 93 additions & 4 deletions

File tree

‎src/Reflection/InitializerExprTypeResolver.php‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2335,8 +2335,13 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri
23352335
$leftNumberType = $leftType->toNumber();
23362336
$rightNumberType = $rightType->toNumber();
23372337

2338+
// a template type of mixed does not absorb the other operand in the union
2339+
$typesMixed = $types instanceof MixedType
2340+
|| $leftType instanceof MixedType
2341+
|| $rightType instanceof MixedType;
2342+
23382343
if (
2339-
!$types instanceof MixedType
2344+
!$typesMixed
23402345
&& (
23412346
$rightNumberType instanceof IntegerRangeType
23422347
|| $rightNumberType instanceof ConstantIntegerType
@@ -2397,14 +2402,14 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri
23972402

23982403
$resultType = TypeCombinator::union($leftNumberType, $rightNumberType);
23992404
if ($expr instanceof Expr\BinaryOp\Div) {
2400-
if ($types instanceof MixedType || $resultType->isInteger()->yes()) {
2405+
if ($typesMixed || $resultType->isInteger()->yes()) {
24012406
return new BenevolentUnionType([new IntegerType(), new FloatType()]);
24022407
}
24032408

24042409
return new UnionType([new IntegerType(), new FloatType()]);
24052410
}
24062411

2407-
if ($types instanceof MixedType
2412+
if ($typesMixed
24082413
|| $leftType instanceof BenevolentUnionType
24092414
|| $rightType instanceof BenevolentUnionType
24102415
) {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15388;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @param list<int> $vals */
8+
function foo(array $vals): void {
9+
assertType('(float|int)', array_reduce($vals, function($carry, $val) {
10+
return $carry + map($val);
11+
}, 0));
12+
assertType('(float|int)', array_reduce($vals, function($carry, $val) {
13+
return $carry + $val;
14+
}, 0));
15+
assertType('(float|int)', array_reduce($vals, function($carry, int $val) {
16+
return $carry - $val;
17+
}, 0));
18+
assertType('(float|int)', array_reduce($vals, function($carry, int $val) {
19+
return $carry * $val;
20+
}, 1));
21+
}
22+
23+
function map(int $val): int {
24+
return $val;
25+
}
26+
27+
/**
28+
* @template T
29+
* @param T $t
30+
* @param mixed $m
31+
*/
32+
function templateOfMixed($t, $m, int $i): void
33+
{
34+
assertType('(float|int)', $m + $i);
35+
assertType('(float|int)', $t + $i);
36+
assertType('(float|int)', $i + $t);
37+
assertType('(float|int)', $t + 1);
38+
assertType('(float|int)', $t - $i);
39+
assertType('(float|int)', $t - 1);
40+
assertType('(float|int)', $t * $i);
41+
assertType('(float|int)', $t / $i);
42+
assertType('(float|int)', $t ** $i);
43+
assertType('(float|int)', $t << $i);
44+
assertType('int', $t % $i);
45+
46+
$a = $t;
47+
$a += $i;
48+
assertType('(float|int)', $a);
49+
50+
$a = $t;
51+
$a++;
52+
assertType('(float|int)', $a);
53+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,13 @@ public function testBug11507(): void
542542
$this->analyse([__DIR__ . '/data/bug-11507.php'], []);
543543
}
544544

545+
public function testBug15388(): void
546+
{
547+
$this->checkNullables = true;
548+
$this->checkExplicitMixed = false;
549+
$this->analyse([__DIR__ . '/data/bug-15388.php'], []);
550+
}
551+
545552
public function testBug15245(): void
546553
{
547554
$this->checkNullables = true;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug15388ReturnType;
4+
5+
/** @param list<int> $vals */
6+
function foo(array $vals): int {
7+
return array_reduce($vals, function($carry, $val) {
8+
return $carry + map($val);
9+
}, 0);
10+
}
11+
12+
/** @param list<int> $vals */
13+
function bar(array $vals): int {
14+
return array_reduce($vals, function($carry, $val) {
15+
return $carry + $val;
16+
}, 0);
17+
}
18+
19+
function map(int $val): int {
20+
return $val;
21+
}

‎turbo-ext/src/InitializerExprTypeResolver.cpp‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2678,7 +2678,10 @@ class InitializerExprTypeResolver
26782678
IETR_VAL(leftNumberType, callOn(leftType, mToNumber));
26792679
IETR_VAL(rightNumberType, callOn(rightType, mToNumber));
26802680

2681-
bool typesMixed = isA(types.raw(), pt_ce_mixed_type);
2681+
// a template type of mixed does not absorb the other operand in the union
2682+
bool typesMixed = isA(types.raw(), pt_ce_mixed_type)
2683+
|| isA(leftType, pt_ce_mixed_type)
2684+
|| isA(rightType, pt_ce_mixed_type);
26822685
if (
26832686
!typesMixed
26842687
&& (

0 commit comments

Comments
 (0)