Skip to content

Treat a template type of mixed operand like mixed in arithmetic so the result stays a benevolent (float|int) - #6806

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-8b4dxl5
Open

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-8b4dxl5

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since 2.3.0, array_reduce($vals, function ($carry, $val) { return $carry + map($val); }, 0) is inferred as strict float|int, so returning it from a function declared : int reports Function foo() should return int but returns float|int. Before, the result was a benevolent (float|int), and $carry + $val still gives one.

The cause is an inconsistency in arithmetic on a template type of mixed: T + int gave strict float|int, while mixed + int and T + U gave benevolent (float|int). This PR makes a template-of-mixed operand behave like mixed.

Changes

  • src/Reflection/InitializerExprTypeResolver.php (resolveCommonMath()): the "operands are mixed" flag now holds when either operand is a MixedType, not only when TypeCombinator::union($left, $right) collapses to MixedType. The flag controls the integer-range shortcut, the benevolent result for /, and the benevolent result for the other operators.
  • turbo-ext/src/InitializerExprTypeResolver.cpp: the same change in the native mirror.

Operators affected: +, -, *, /, <<, >>, plus their compound assignments and ++/--.

Checked and already consistent between mixed and T: **, %, unary -/+ (strict float|int for both), T + U and the array + paths (these already test instanceof MixedType on each operand).

Root cause

TypeCombinator::union(T, int) keeps T|int because a template type does not absorb other types, so $types instanceof MixedType was false for T + int. The result therefore came out as a strict union.

In 2.3 the closure passed to array_reduce() is typed from the callable(TReturn, TIn): TReturn parameter. While the closure type is gathered for template inference, $carry is the template TReturn (of mixed). $carry + map($val) became a strict float|int, so TReturn resolved to strict float|int, and that strict union reached the return type. In the $carry + $val variant both operands were templates, which already hit the MixedType && MixedType branch, so the result stayed benevolent.

Test

  • tests/PHPStan/Analyser/nsrt/bug-15388.php: array_reduce() with +, - and * callbacks, and arithmetic on a template of mixed (+, -, *, /, **, <<, %, +=, ++) compared with mixed.
  • tests/PHPStan/Rules/Functions/data/bug-15388.php + ReturnTypeRuleTest::testBug15388(): the issue's reproducer, which should report no errors.

Both tests fail without the fix.

Fixes phpstan/phpstan#15388

🤖 Generated with Claude Code

…o 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>
@ondrejmirtes
ondrejmirtes force-pushed the create-pull-request/patch-8b4dxl5 branch from dadb7da to b13462d Compare October 27, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2.3.0: array_reduce returning int now returns float|int if part of the result is a function

1 participant