Repository navigation
Bind intrinsic callback-argument overrides to parameters by name, not by written argument position - #6954
Bind intrinsic callback-argument overrides to parameters by name, not by written argument position#6954phpstan-bot wants to merge 5 commits into
Conversation
… 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>
staabm
left a comment
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
| * | ||
| * @param Arg[] $args | ||
| */ | ||
| public static function getArgByPositionOrName(array $args, int $position, string $name): ?Arg |
There was a problem hiding this comment.
we miss some unit tests for this method
There was a problem hiding this comment.
I added the unit tests for ArgumentsNormalizer::getArgByPositionOrName(). Running them with the turbo extension loaded turned up a difference between the PHP method and its C++ mirror, which I fixed. Both commits are pushed.
Tests (6f45d0882, tests/PHPStan/Analyser/ArgumentsNormalizerTest.php): testGetArgByPositionOrName takes its cases from a data provider:
- no arguments;
- a positional argument at the position, and too few positional arguments;
- a named argument in signature order, and in another order (
callback:, array:andmode:, callback:, array:); - positional arguments followed by the named one;
- a named argument sitting at the position but belonging to a different parameter, which must return
null; - argument names are case-sensitive;
- a negative position.
Fix (c2e266143, turbo-ext/src/ArgumentsNormalizer.cpp): with a negative position, the native binding returned null straight away. The PHP version still finds an argument passed by name in that case, for example ([x, array: y], -1, 'array') returns y. I removed the early return. The C++ now compares the array key the same way PHP's $i === $position does, so both give the same result.
Checks:
- With the extension loaded, the new tests now pass, as do
tests/smoke.phpandtests/signature-parity.php. make testspasses (23417 tests, 107 skipped), and so domake phpstanandmake cs.
On the broader point about repeating the parameter-position and argument handling from FunctionCallParametersCheck: I left that alone. Merging it into one shared helper would be a refactor beyond this PR.
…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>
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