Fix phpstan/phpstan#7280: array_reduce could infer the precise type of the $initial argument within the callable and in the return type#5168
Conversation
- Added ArrayReduceCallbackClosureTypeExtension implementing FunctionParameterClosureTypeExtension - The extension provides the initial argument type (generalized to remove literals) as the carry parameter type - The array's value type is provided as the value parameter type - New regression test in tests/PHPStan/Analyser/nsrt/bug-7280.php Closes phpstan/phpstan#7280
| $result1 = array_reduce( | ||
| ['test1', 'test2'], | ||
| static function (array $carry, string $value): array { | ||
| assertType("array{starts: array{}, ends: array{}}", $carry); |
There was a problem hiding this comment.
I think this type is wrong. it only is correct for the first invocation when initial is used.
in this example, I think we need to aim for `assertType("array{starts: array, ends: array}", $carry);
| $result3 = array_reduce( | ||
| [1, 2, 3], | ||
| static function (?int $carry, int $value): int { | ||
| assertType('null', $carry); |
There was a problem hiding this comment.
| assertType('null', $carry); | |
| assertType('int|null', $carry); |
VincentLanglet
left a comment
There was a problem hiding this comment.
This is wrong
assertType("array{starts: array{}, ends: array{}}", $carry);
and
assertType('null', $carry);
are only true for the first iteration but not for the following one.
The inferred type should be closer to
assertType("array{starts: array, ends: array}", $carry);
and
assertType('int|null', $carry);
The carry parameter type should be the union of the initial value type
and the callback's return type, since after the first iteration $carry
receives the callback's return value, not the initial value. Previously,
only the initial type was used, which was incorrect for subsequent
iterations (e.g., array{starts: array{}, ends: array{}} was wrong when
the callback returns a modified array, and null was wrong when the
callback returns int).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Done. Here's what I changed: Problem: The Fix in Test changes:
|
Summary
array_reducenow infers the precise type of the$initialargument as the$carryparameter type within the callback. Previously,$carrywas typed asmixed(from the function signature), which when intersected with the closure's native type hint gave only broad types likearrayornon-empty-array. Now, constant array shapes likearray{starts: array{}, ends: array{}}are preserved inside the callback.Changes
src/Type/Php/ArrayReduceCallbackClosureTypeExtension.php— a newFunctionParameterClosureTypeExtensionthat overrides the callback parameter types forarray_reduce:0→int)tests/PHPStan/Analyser/nsrt/bug-7280.php— regression test covering:Root cause
The function signature for
array_reduceinfunctionMap.phpdeclares the callback ascallable(mixed,mixed):mixed. This meant the carry parameter had no type information beyondmixed, so PHPStan could only use the closure's native type hints. By implementing aFunctionParameterClosureTypeExtension, the initial argument's type is now propagated to the carry parameter, which is then intersected with the native type hint to produce precise types inside the callback body.Test
The regression test verifies that:
$carryinside a callback withinitial: ['starts' => [], 'ends' => []]is typed asarray{starts: array{}, ends: array{}}(wasnon-empty-array)0and''are generalized tointandstring(no over-narrowing)nullFixes phpstan/phpstan#7280