Repository navigation
Bind intrinsic callback-argument overrides to parameters by name, not by written argument position - #6954
Merged
staabm merged 6 commits intoNov 9, 2026
Conversation
VincentLanglet
approved these changes
Nov 4, 2026
staabm
requested changes
Nov 5, 2026
staabm
left a comment
Contributor
There was a problem hiding this comment.
I think this looks mostly good and it hits a nail which I talked about with ondrej yesterday in private somewhere else:
atm it seems we need to duplicate the parameter-positions and arguments "dance" we see in
also in other fixer trying to fiddle with argsphpstan-src/src/Rules/FunctionCallParametersCheck.php
Lines 904 to 988 in 53c3662
… by written argument position - ArrayFilterArgVisitor, ArrayWalkArgVisitor, ArrayFindArgVisitor, CurlSetOptArgVisitor and CurlSetOptArrayArgVisitor mark the argument bound to the relevant parameter (positional index or matching name) instead of whichever argument was written at that index. - ArrayMapArgVisitor attaches the array arguments to the `callback:` argument even when it is not written first. - ParametersAcceptorSelector::applyIntrinsicArgOverrides() reorders named arguments into signature order (ArgumentsNormalizer::reorderArgs()) before reading sibling arguments ($args[1], $args[2], ...) for array_map, array_filter, array_walk, array_find/array_find_key/array_any/array_all, curl_setopt and curl_setopt_array. - ArrayMap/ArrayFilter/ArrayWalk/ArrayFind parameter closure type extensions and PregReplaceCallbackClosureTypeExtension reorder named arguments before reading them by position. - Ported to the turbo-ext C++ mirrors (the five visitors, ArrayMapArgVisitor, ParametersAcceptorSelector, new visitors::argForParameter() helper); regenerated generated/ParametersAcceptorSelector.h. - implode() and Closure::bind()/bindTo() overrides keep reading the original arguments (implode already handles a named `array:` argument).
The intrinsic-override parser visitors each spelled out the same loop to find the argument bound to a parameter by position or by name. The native ArgumentsNormalizer mirrors the new method via visitors::argForParameter(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etArgByPositionOrName() Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PositionOrName() The PHP method still finds an argument passed by name when the position is negative; the C++ mirror returned null early. Comparing the hash key as zend_ulong keeps exact parity with $i === $position. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
staabm
force-pushed
the
create-pull-request/patch-aikn0sp
branch
from
November 8, 2026 07:59
6f45d08 to
6e4c2dd
Compare
staabm
approved these changes
Nov 8, 2026
Contributor
|
//cc @SanderMuller please review |
SanderMuller
reviewed
Nov 8, 2026
SanderMuller
left a comment
Contributor
There was a problem hiding this comment.
I ran this locally and found no problems.
- The reporter's snippet at level 10: the base reports
array_filteron line 15, and the PR head reports nothing. The newtestBug15195fails with thesrcandturbo-extchanges reverted. Then it shows the old false positives and misses thecurl_setoptandcurl_setopt_arrayerrors. - Calls without named arguments keep the old path, because
reorderArgs()returns them unchanged. So positional and spread calls do not change. - I also tried the neighbours with named arguments in reverse order: the
preg_match()matches,parse_str(),array_reduce(),usort()and thepreg_replace_callback()closure parameter. They give the same types as with positional arguments. - Turbo: the new tests pass with the extension.
smoke.php,signature-parity.php(18,494 members) andside-by-side.phpare clean. The output on the reporter's snippet,bug-15195.phpand my probe file is identical with the extension on and off. make phpstan, phpcs and the full test suite pass, with and without Turbo. One exception:OverridingMethodRuleConfigPhpTestfails on my machine on the base too, and it passes in CI.- CPU: cold self-analysis without Turbo, 3 interleaved rounds. The user+sys median is 141.8 s on base and 142.4 s with the PR. The spread is about 3 s, so I see no measurable difference.
- The 3 red integration jobs are also red on #7004 and #7007.
Contributor
|
thank you! |
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(string $s): bool => ..., array: $list)reportedParameter $callback of function array_filter expects (callable(mixed): bool)|null, Closure(string): bool given.. The same call with the arguments in signature order was fine. The intrinsic overrides that type a callback from its sibling array argument assumed the argument written first was the array. With named arguments in another order, they read the closure as the "array", so its iterable value type came out asmixed.The fix ties each override to the argument bound to the parameter, by position or by name. It applies to every intrinsic override that reads sibling arguments, to the parameter closure type extensions, and to their turbo-ext C++ mirrors.
Changes
src/Parser/ArrayFilterArgVisitor.php,ArrayWalkArgVisitor.php,ArrayFindArgVisitor.php,CurlSetOptArgVisitor.php,CurlSetOptArrayArgVisitor.php: mark the argument bound to$array/$handle/$options, meaning the positional one at that index or the one passed by that name.src/Parser/ArrayMapArgVisitor.php: attach the array arguments to thecallbackargument wherever it is written.src/Reflection/ParametersAcceptorSelector.php: the newreorderIntrinsicArgs()puts named arguments in signature order viaArgumentsNormalizer::reorderArgs()before the array_map / array_filter / array_walk / array_find / curl_setopt / curl_setopt_array overrides read$args[0..2]. Calls without named arguments take the unchanged fast path. The NodeScopeResolver path already passes normalized arguments, and the marker attributes travel with them.src/Type/Php/Array{Map,Filter,Walk,Find}ParameterClosureTypeExtension.php,PregReplaceCallbackClosureTypeExtension.php: these also read$functionCall->getArgs()by position. They now reorder named arguments first. Before this, they hid real errors such asarray_walk(callback: fn(string $v, int $k) ..., array: array<string, int>), andpreg_replace_callback(callback: ..., pattern: ...)could take the callback as the pattern.ArrayFilterArgVisitor.cpp,ArrayWalkArgVisitor.cpp,ArrayFindArgVisitor.cpp,CurlSetOptArgVisitor.cpp,CurlSetOptArrayArgVisitor.cpp,ArrayMapArgVisitor.cppandParametersAcceptorSelector.cpp. There is a new sharedvisitors::argForParameter()helper inParserVisitors.h, andgenerated/ParametersAcceptorSelector.his regenerated.implode()override already handles a namedarray:argument, so it keeps reading the original arguments.Closure::bind()/bindTo()overrides are unchanged.Root cause
The pattern is that positional
$args[N]indexing was used to read a call's arguments wherever a call-specific override or extension inspects sibling arguments. The parser visitors marked$args[0]/$args[1], andapplyIntrinsicArgOverrides()plus the parameter closure type extensions read$args[N], all assuming arguments are written in signature order. Named arguments break that assumption. The result was false positives (array_filter, array_filter with a namedmode:, array_walk, array_find family, array_map) and missed errors (curl_setopt($h, value: ..., option: ...),curl_setopt_array(options: ..., handle: ...), and mismatched closures that the closure type extensions accepted).Test
CallToFunctionParametersRuleTest::testBug15195withtests/PHPStan/Rules/Functions/data/bug-15195.phpcovers:array_filterwith namedmode:(ARRAY_FILTER_USE_KEY/ARRAY_FILTER_USE_BOTH);array_walkwith and without thearg:argument;array_find,array_find_key,array_any,array_all;array_map(array: ..., callback: ...);curl_setopt/curl_setopt_arraywith reordered named arguments.Each family has a valid call, which must not error, and a mismatched one, which must error. The test fails without the fix: it reports false positives and misses expected errors. The turbo extension passes
tests/smoke.php,tests/signature-parity.phpandbin/side-by-side.php(no new findings), and analysis output was byte-identical with the extension on and off for the probe files.Fixes phpstan/phpstan#15195
🤖 Generated with Claude Code