Resolve intrinsic argument overrides by parameter position instead of the order arguments are written - #6488
Open
phpstan-bot wants to merge 3 commits into
Open
phpstan-bot wants to merge 3 commits into
phpstan-bot wants to merge 3 commits into
Conversation
VincentLanglet
approved these changes
Sep 20, 2026
staabm
requested changes
Sep 22, 2026
staabm
left a comment
Contributor
There was a problem hiding this comment.
needs rebase and merge conflict resolution.
meanwhile *Vistor classes also have a corresponding .cpp impl which might need the changes mirrored?
… the order arguments are written * Added `PHPStan\Parser\ArgumentPositionHelper::getArgsByPosition()`, which maps a call's arguments onto the positions of the callee's parameters, honouring named arguments written out of order. * Every intrinsic arg visitor (`ArrayFilterArgVisitor`, `ArrayMapArgVisitor`, `ArrayWalkArgVisitor`, `ArrayFindArgVisitor`, `ImplodeArgVisitor`, `CurlSetOptArgVisitor`, `CurlSetOptArrayArgVisitor`, `ClosureBindArgVisitor`, `ClosureBindToVarVisitor`) now declares a `PARAMETER_NAMES` constant and marks the argument that actually fills the parameter it cares about, instead of the argument that happens to be written first. * `ParametersAcceptorSelector::applyIntrinsicArgOverrides()` reads its sibling arguments through the same mapping, so the array / option / callback / closure argument is found wherever it is written. * `ArrayMapArgVisitor` now attaches `arrayMapArgs` to the callback argument's value in all argument orders, which is where `ContextualClosureParameterResolver` and `ScopeOps::nodeKey()` expect it. * Fixed the same order-dependence for `array_map`, `array_walk`, `array_find`/`array_any`/`array_all`/`array_find_key`, `curl_setopt`, `curl_setopt_array` and `Closure::bind`; `Closure::bindTo` and `implode` were probed and were already order-independent, but go through the shared mapping now too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ArgumentPositionHelper was a separate class for what is argument reordering logic, which already has a home. Move the method there and share the "does this call use named arguments" check with reorderArgs(), which was scanning for it inline. The two remain distinct: reorderArgs() needs a ParametersAcceptor and produces copies of the Arg nodes, while getArgsByPosition() takes the parameter names spelled out by the caller, so it also works in the parser visitors where no reflection is available yet, and returns the original Arg objects so the attributes those visitors set land on the analysed AST. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The class is already marked @api, so the @internal on this one method was the odd one out. Visitors written outside phpstan-src face the same named-argument problem the intrinsic arg visitors do, so let them use it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
VincentLanglet
force-pushed
the
create-pull-request/patch-vj6os0g
branch
from
September 22, 2026 12:14
17e5fce to
cbe8eac
Compare
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.
Summary
array_filter(callback: $fn, array: $arr)reported a false positiveParameter $callback of function array_filter expects (callable(mixed): bool)|null, Closure(string): bool given., while the same call written asarray_filter(array: $arr, callback: $fn)analysed cleanly.The callback's expected signature is not in the function map - it is synthesized at analysis time from the type of the array argument. That synthesis looked the array argument up as
$args[0], i.e. by its position in the source, which named arguments are free to change. The same holds for every other intrinsic argument override PHPStan applies.Changes
PHPStan\Parser\ArgumentPositionHelper::getArgsByPosition()(src/Parser/ArgumentPositionHelper.php) - maps a call's arguments onto the positions of the callee's parameters. It is a no-op (returns the arguments unchanged) when no named arguments are in play, so the hot path is unaffected.src/Parser/gained aPARAMETER_NAMESconstant and now marks the argument that fills the parameter it cares about:ArrayFilterArgVisitor,ArrayWalkArgVisitor,ArrayFindArgVisitor,CurlSetOptArgVisitor- the first parameterCurlSetOptArrayArgVisitor- theoptionsparameterClosureBindArgVisitor- theclosureparameter,ClosureBindToVarVisitor- thenewThisparameterImplodeArgVisitor- theseparatorparameter, falling back toarrayforimplode(array: $a)ArrayMapArgVisitor- thecallbackargument's value (previously the source-first argument's value, which is not the closure once the arguments are reordered;ContextualClosureParameterResolverandScopeOps::nodeKey()both look the attribute up on the closure node)ParametersAcceptorSelector::applyIntrinsicArgOverrides()(src/Reflection/ParametersAcceptorSelector.php) reads all of its sibling arguments ($args[0],$args[1],$args[2]) through the same mapping instead of indexing the call's arguments directly.Analogous cases fixed alongside the reported one, each with its own failing test:
array_filter(callback: …, array: …)callable(mixed): bool(reported bug)array_filter(mode: …, callback: …, array: …)ARRAY_FILTER_USE_KEY/ARRAY_FILTER_USE_BOTHnot honoured, callback typedcallable(mixed): boolarray_map(array: …, callback: …)arrayMapArgsattached to the array expression rather than the callbackarray_walk(callback: …, array: …)callable(mixed, mixed): mixedarray_walk(arg: …, callback: …, array: …)array_find/array_any/array_all/array_find_keywith reversed named argumentscallable(mixed, mixed): boolcurl_setopt(option: …, handle: …, value: …)CURLOPT_*value type not narrowed (false negative)curl_setopt_array(options: …, handle: …)Closure::bind(newThis: …, closure: …)@param-closure-thisnot enforced (false negative)Probed and found already order-independent (they go through the shared mapping now, but their behaviour is unchanged):
Closure::bindTowithnewScope:written beforenewThis:,implode/joinin every named-argument order, and the closure parameter type inference performed by theFunctionParameterClosureTypeExtensions (which already resolved named arguments correctly).Root cause
A parser visitor tags one argument of a well-known call with an attribute;
ParametersAcceptorSelector::applyIntrinsicArgOverrides()then detects that attribute and reads the other arguments by index to synthesize a more precise parameter type (the callback signature for the array functions, theCURLOPT_*value type for curl, the@param-closure-thistype forClosure::bind).Both halves assumed the source position of an argument equals the position of the parameter it fills. Named arguments break that assumption in two different ways depending on the caller:
ParametersAcceptorSelector::selectFromArgs()passes the arguments as written, so the marker was found but the sibling reads picked up the wrong arguments.ArgumentsHandlerpasses arguments already reordered byArgumentsNormalizer, so the marker travelled with the argument it was set on and ended up at the wrong index, and the override was skipped entirely.Resolving both the marking and the reads through a signature-position mapping makes the two halves agree in either case, and makes the analysis independent of the order named arguments are written in.
Test
tests/PHPStan/Rules/Functions/data/bug-15195.php- the reproducer from the issue's playground link verbatim, expected to analyse clean (CallToFunctionParametersRuleTest::testBug15195, with explicit/implicit mixed checks on to match the playground's level 10).tests/PHPStan/Rules/Functions/data/named-arguments-order-intrinsic.php-array_filter(plain,ARRAY_FILTER_USE_KEY,ARRAY_FILTER_USE_BOTH),array_map,array_walk(with and withoutarg:),curl_setopt,curl_setopt_arrayandimplodewith named arguments in reversed order; each construct has a correct call that must stay clean and a deliberately wrong one that must be reported.tests/PHPStan/Rules/Functions/data/named-arguments-order-array-find.php- the same forarray_find,array_find_key,array_anyandarray_all(PHP 8.4).tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this-named-args.phpand.../closure-bind-to-param-closure-this-named-args.php-@param-closure-thisenforcement forClosure::bind()andClosure::bindTo()in both named-argument orders.All of these were confirmed to fail before the fix (false positives on the correct calls, missing errors on the wrong ones) and pass after it.
make tests,make phpstanandmake csare green.Fixes phpstan/phpstan#15195
🤖 Generated with Claude Code