Infer array_reduce() callback parameter types from the initial and array arguments#5978
Open
gnutix wants to merge 1 commit into
Open
Infer array_reduce() callback parameter types from the initial and array arguments#5978gnutix wants to merge 1 commit into
gnutix wants to merge 1 commit into
Conversation
…ray arguments Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes phpstan/phpstan#7280
Description
Inside an
array_reduce()callback,$carrywas typed from the naive closure analysis (non-empty-arrayin the linked issue) instead of from the$initialargument, and the call's result type was correspondingly imprecise.Root cause
array_reduce()'s callback parameter type is recursive —stubs/arrayFunctions.stubtypes itcallable(TReturn, TIn): TReturn— and PHPStan has no fixed-point machinery to resolve it:ParametersAcceptorSelector::selectFromArgs()types the closure argument naively viaClosureTypeResolver::getClosureType(), using only the declared parameter hints.GenericParametersAcceptorResolverthen unions that polluted return type intoTReturn, whichNodeScopeResolverpropagates into the callback body.ArrayReduceFunctionReturnTypeExtensionreads the same garbage-in closure type for the call's result.Unlike
array_map(), there was no visitor connecting the callback to the other arguments of the call (ArrayMapArgVisitoris howarray_map()callbacks get precise parameter types).Design
A new
ArrayReduceArgVisitorattaches the array and initial arguments (positional or named — the issue's own reproducer usesinitial:) to the callback expression, skipping first-class-callablearray_reduce(...)and argument unpacking.ClosureTypeResolver::getClosureType()then resolves the callback with a bounded fixed-point iteration, for both closures and arrow functions:$carry= type of$initialand$item= the array's iterable value type. If the resulting return type is a subtype of the carry type, the carry type is a fixed point and we are done.union(initial, return), generalized shape-preservingly (ConstantArrayType::generalizeValues()when all union members are constant arrays,generalize(GeneralizePrecision::lessSpecific())otherwise), and pass 2 re-analyses the body with the widened carryW.f(W) ⊆ Wis the widened type accepted. If even the widened type is not a fixed point (e.g. a nesting callback likefn ($carry, $v) => ['x' => $carry], whose result grows without bound), the callback is analysed once more with the parameter types it would have had without this feature — i.e. the current behaviour is kept verbatim, instead of claiming an unsound shape.ParametersAcceptorSelector::selectFromArgs()(mirroring itsarray_map()branch) overrides the callback parameter ofarray_reduce()tocallable(union(initial, callbackReturn), itemType): TReturn, keeping the declared return type so existing rule messages don't change. This is what rules and the callback body see, so$carryinside the body becomes the union of the initial type and the callback's (fixed-point) return type.MutatingScope::getNodeKey()includes the new attribute in the expression cache key for the same reasonarrayMapArgsis included — cached closure types must not leak between contexts.ArrayReduceFunctionReturnTypeExtensionis unchanged; it now simply reads a precise callback type.For the issue's reproducer this infers:
Prior art
#5168 attempted this with a
FunctionParameterClosureTypeExtensionand was closed unmerged after review: typing$carryas the initial type is only correct for the first iteration, and unioning with the declared return type destroyed precision. This PR answers that soundness objection differently: the carry type isinitial ∪ callbackReturn(exactly the union the reviewers asked for), the callback return comes from a verified fixed point rather than from declared hints, and when no fixed point is reached the implementation falls back to today's behaviour entirely. The extension-only route also could not fix the result type, because the return type extension reads the naive closure type, which never consults parameter-closure-type extensions.Performance
array_reduce()callback bodies are now analysed up to two times insidegetClosureType()(a third pass happens only for the rare non-converging callbacks, to restore the naive behaviour). Non-array_reducepaths throughClosureTypeResolverare untouched.Ecosystem note
Every
array_reduce()user's carry/result types become more precise, so downstream projects may see new findings (e.g. the result of a summing reduce is nowintinstead of a benevolent(array|float|int)).Existing expectations survive unchanged: the six
array_reducereturn types intests/PHPStan/Analyser/nsrt/array-functions.php, the sixCallToFunctionParametersRulemessages (identical carry/item types), andtests/PHPStan/Rules/Operators/data/bug-7280-comment.php.🤖 Generated with Claude Code, model Fable 5.