From e31137e3161187829621d0900b429f09120ed714 Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Sun, 20 Sep 2026 19:01:13 +0000 Subject: [PATCH 1/3] Resolve intrinsic argument overrides by parameter position instead of 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 --- src/Parser/ArgumentPositionHelper.php | 65 +++++++++++++++ src/Parser/ArrayFilterArgVisitor.php | 4 +- src/Parser/ArrayFindArgVisitor.php | 4 +- src/Parser/ArrayMapArgVisitor.php | 22 ++--- src/Parser/ArrayWalkArgVisitor.php | 4 +- src/Parser/ClosureBindArgVisitor.php | 7 +- src/Parser/ClosureBindToVarVisitor.php | 4 +- src/Parser/CurlSetOptArgVisitor.php | 4 +- src/Parser/CurlSetOptArrayArgVisitor.php | 4 +- src/Parser/ImplodeArgVisitor.php | 10 ++- src/Reflection/ParametersAcceptorSelector.php | 81 +++++++++++-------- .../CallToFunctionParametersRuleTest.php | 75 +++++++++++++++++ .../Rules/Functions/data/bug-15195.php | 20 +++++ .../data/named-arguments-order-array-find.php | 18 +++++ .../data/named-arguments-order-intrinsic.php | 57 +++++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 19 +++++ .../Methods/CallStaticMethodsRuleTest.php | 18 +++++ ...ure-bind-param-closure-this-named-args.php | 21 +++++ ...-bind-to-param-closure-this-named-args.php | 19 +++++ 19 files changed, 404 insertions(+), 52 deletions(-) create mode 100644 src/Parser/ArgumentPositionHelper.php create mode 100644 tests/PHPStan/Rules/Functions/data/bug-15195.php create mode 100644 tests/PHPStan/Rules/Functions/data/named-arguments-order-array-find.php create mode 100644 tests/PHPStan/Rules/Functions/data/named-arguments-order-intrinsic.php create mode 100644 tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this-named-args.php create mode 100644 tests/PHPStan/Rules/Methods/data/closure-bind-to-param-closure-this-named-args.php diff --git a/src/Parser/ArgumentPositionHelper.php b/src/Parser/ArgumentPositionHelper.php new file mode 100644 index 00000000000..aaf047bd18e --- /dev/null +++ b/src/Parser/ArgumentPositionHelper.php @@ -0,0 +1,65 @@ + $parameterNames parameter names in signature order + * @return array + */ + public static function getArgsByPosition(array $args, array $parameterNames): array + { + $hasNamedArgs = false; + foreach ($args as $arg) { + if ($arg->name === null) { + continue; + } + + $hasNamedArgs = true; + break; + } + + if (!$hasNamedArgs) { + return $args; + } + + $argsByPosition = []; + foreach ($args as $i => $arg) { + if ($arg->name === null) { + // positional arguments always precede named ones + $argsByPosition[$i] = $arg; + continue; + } + + $position = array_search($arg->name->toString(), $parameterNames, true); + if ($position === false) { + continue; + } + + $argsByPosition[$position] = $arg; + } + + return $argsByPosition; + } + +} diff --git a/src/Parser/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php index 09b11ff3ad6..83c72f0d27e 100644 --- a/src/Parser/ArrayFilterArgVisitor.php +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -15,13 +15,15 @@ final class ArrayFilterArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isArrayFilterArg'; + public const PARAMETER_NAMES = ['array', 'callback', 'mode']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_filter') { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php index ca52b483b72..b039fbf5af9 100644 --- a/src/Parser/ArrayFindArgVisitor.php +++ b/src/Parser/ArrayFindArgVisitor.php @@ -16,13 +16,15 @@ final class ArrayFindArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isArrayFindArg'; + public const PARAMETER_NAMES = ['array', 'callback']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php index b0b35180d63..4001abd2bdd 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -17,6 +17,8 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'arrayMapArgs'; + public const PARAMETER_NAMES = ['callback', 'array', 'arrays']; + #[Override] public function enterNode(Node $node): ?Node { @@ -24,22 +26,22 @@ public function enterNode(Node $node): ?Node $functionName = $node->name->toLowerString(); if ($functionName === 'array_map') { $args = $node->getArgs(); + $callbackArg = ArgumentPositionHelper::getArgsByPosition($args, self::PARAMETER_NAMES)[0] ?? null; + if ($callbackArg === null) { + return null; + } + $arrayArgs = []; - foreach ($args as $i => $arg) { - if ($arg->name === null && $i === 0) { - continue; - } - if ($arg->name !== null && $arg->name->toString() === 'callback') { + foreach ($args as $arg) { + if ($arg === $callbackArg) { continue; } $arrayArgs[] = $arg; } - if (isset($args[0])) { - $slicedArgs = array_slice($args, 1); - if (count($slicedArgs) > 0) { - $args[0]->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs); - } + + if (count($arrayArgs) > 0) { + $callbackArg->value->setAttribute(self::ATTRIBUTE_NAME, $arrayArgs); } } } diff --git a/src/Parser/ArrayWalkArgVisitor.php b/src/Parser/ArrayWalkArgVisitor.php index 0cc945a67a0..cdb72f916d6 100644 --- a/src/Parser/ArrayWalkArgVisitor.php +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -15,13 +15,15 @@ final class ArrayWalkArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isArrayWalkArg'; + public const PARAMETER_NAMES = ['array', 'callback', 'arg']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_walk') { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php index 93c22b7c615..1df3da871ef 100644 --- a/src/Parser/ClosureBindArgVisitor.php +++ b/src/Parser/ClosureBindArgVisitor.php @@ -17,6 +17,8 @@ final class ClosureBindArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'closureBindArg'; + public const PARAMETER_NAMES = ['closure', 'newThis', 'newScope']; + #[Override] public function enterNode(Node $node): ?Node { @@ -30,7 +32,10 @@ public function enterNode(Node $node): ?Node ) { $args = $node->getArgs(); if (count($args) > 1) { - $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + $args = ArgumentPositionHelper::getArgsByPosition($args, self::PARAMETER_NAMES); + if (isset($args[0])) { + $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + } } } return null; diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php index 9243e6d7848..b429a6a4780 100644 --- a/src/Parser/ClosureBindToVarVisitor.php +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -16,6 +16,8 @@ final class ClosureBindToVarVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'closureBindToVar'; + public const PARAMETER_NAMES = ['newThis', 'newScope']; + #[Override] public function enterNode(Node $node): ?Node { @@ -25,7 +27,7 @@ public function enterNode(Node $node): ?Node && $node->name->toLowerString() === 'bindto' && !$node->isFirstClassCallable() ) { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var); } diff --git a/src/Parser/CurlSetOptArgVisitor.php b/src/Parser/CurlSetOptArgVisitor.php index 748adc8b14e..58784d582b2 100644 --- a/src/Parser/CurlSetOptArgVisitor.php +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -15,13 +15,15 @@ final class CurlSetOptArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isCurlSetOptArg'; + public const PARAMETER_NAMES = ['handle', 'option', 'value']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt') { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/CurlSetOptArrayArgVisitor.php b/src/Parser/CurlSetOptArrayArgVisitor.php index 694fc983632..4338b34ec93 100644 --- a/src/Parser/CurlSetOptArrayArgVisitor.php +++ b/src/Parser/CurlSetOptArrayArgVisitor.php @@ -15,13 +15,15 @@ final class CurlSetOptArrayArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isCurlSetOptArrayArg'; + public const PARAMETER_NAMES = ['handle', 'options']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt_array') { - $args = $node->getArgs(); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[1])) { $args[1]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ImplodeArgVisitor.php b/src/Parser/ImplodeArgVisitor.php index ed3008f6c13..6de4f0343c4 100644 --- a/src/Parser/ImplodeArgVisitor.php +++ b/src/Parser/ImplodeArgVisitor.php @@ -16,15 +16,19 @@ final class ImplodeArgVisitor extends NodeVisitorAbstract public const ATTRIBUTE_NAME = 'isImplodeArg'; + public const PARAMETER_NAMES = ['separator', 'array']; + #[Override] public function enterNode(Node $node): ?Node { if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['implode', 'join'], true)) { - $args = $node->getArgs(); - if (isset($args[0])) { - $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + // implode(array: $a) leaves the first parameter unfilled + $markedArg = $args[0] ?? $args[1] ?? null; + if ($markedArg !== null) { + $markedArg->setAttribute(self::ATTRIBUTE_NAME, true); } } } diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 50481e5b43d..967b9241c4d 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -9,6 +9,7 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; +use PHPStan\Parser\ArgumentPositionHelper; use PHPStan\Parser\ArrayFilterArgVisitor; use PHPStan\Parser\ArrayFindArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; @@ -204,7 +205,7 @@ public static function applyIntrinsicArgOverrides( count($args) > 0 && count($parametersAcceptors) > 0 ) { - $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $arrayMapArgs = (ArgumentPositionHelper::getArgsByPosition($args, ArrayMapArgVisitor::PARAMETER_NAMES)[0] ?? null)?->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callbackParameters = []; $nativeCallbackParameters = []; @@ -252,8 +253,13 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { - $optType = ($typeGetter)($args[1]->value); + $curlSetOptArgs = ArgumentPositionHelper::getArgsByPosition($args, CurlSetOptArgVisitor::PARAMETER_NAMES); + if ( + count($args) >= 3 + && isset($curlSetOptArgs[0], $curlSetOptArgs[1]) + && (bool) $curlSetOptArgs[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME) + ) { + $optType = ($typeGetter)($curlSetOptArgs[1]->value); $valueTypes = []; foreach ($optType->getConstantScalarValues() as $scalarValue) { @@ -296,8 +302,12 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { - $optArrayType = ($typeGetter)($args[1]->value); + $curlSetOptArrayArgs = ArgumentPositionHelper::getArgsByPosition($args, CurlSetOptArrayArgVisitor::PARAMETER_NAMES); + if ( + isset($curlSetOptArrayArgs[1]) + && (bool) $curlSetOptArrayArgs[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME) + ) { + $optArrayType = ($typeGetter)($curlSetOptArrayArgs[1]->value); $hasTypes = false; $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -348,27 +358,28 @@ public static function applyIntrinsicArgOverrides( } } - if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { + $arrayFilterArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayFilterArgVisitor::PARAMETER_NAMES); + if (isset($arrayFilterArgs[0]) && (bool) $arrayFilterArgs[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { $arrayFilterParameters = null; $nativeArrayFilterParameters = null; - if (isset($args[2])) { - $mode = ($typeGetter)($args[2]->value); + if (isset($arrayFilterArgs[2])) { + $mode = ($typeGetter)($arrayFilterArgs[2]->value); if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($typeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($typeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; $nativeArrayFilterParameters = [ - new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), - new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($args[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('item', ($iterableValueTypeGetter)(($nativeTypeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), + new DummyParameter('key', ($iterableKeyTypeGetter)(($nativeTypeGetter)($arrayFilterArgs[0]->value)), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; } } @@ -377,7 +388,7 @@ public static function applyIntrinsicArgOverrides( $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $arrayArgType = ($typeGetter)($args[0]->value); + $arrayArgType = ($typeGetter)($arrayFilterArgs[0]->value); $callableType = new UnionType([ new CallableType( $arrayFilterParameters ?? [ @@ -388,7 +399,7 @@ public static function applyIntrinsicArgOverrides( ), new NullType(), ]); - $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($arrayFilterArgs[0]->value); $nativeCallableType = new UnionType([ new CallableType( $nativeArrayFilterParameters ?? [ @@ -404,11 +415,13 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) <= 2 && (bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + $implodeArgs = ArgumentPositionHelper::getArgsByPosition($args, ImplodeArgVisitor::PARAMETER_NAMES); + $implodeMarkedArg = $implodeArgs[0] ?? $implodeArgs[1] ?? null; + if (count($args) <= 2 && $implodeMarkedArg !== null && (bool) $implodeMarkedArg->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { $acceptor = $namedArgumentsVariants[0] ?? $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if ( - (isset($args[1]) || ($args[0]->name !== null && $args[0]->name->name === 'array')) + isset($implodeArgs[1]) && isset($parameters[0]) && isset($parameters[1]) ) { $parameters = [ @@ -433,9 +446,10 @@ public static function applyIntrinsicArgOverrides( ]; } - if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { - $arrayArgType = ($typeGetter)($args[0]->value); - $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); + $arrayWalkArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayWalkArgVisitor::PARAMETER_NAMES); + if (isset($arrayWalkArgs[0]) && (bool) $arrayWalkArgs[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { + $arrayArgType = ($typeGetter)($arrayWalkArgs[0]->value); + $nativeArrayArgType = ($nativeTypeGetter)($arrayWalkArgs[0]->value); $arrayWalkParameters = [ new DummyParameter('item', ($iterableValueTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), new DummyParameter('key', ($iterableKeyTypeGetter)($arrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), @@ -444,9 +458,9 @@ public static function applyIntrinsicArgOverrides( new DummyParameter('item', ($iterableValueTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createReadsArgument(), variadic: false, defaultValue: null), new DummyParameter('key', ($iterableKeyTypeGetter)($nativeArrayArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), ]; - if (isset($args[2])) { - $arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); - $nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($args[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + if (isset($arrayWalkArgs[2])) { + $arrayWalkParameters[] = new DummyParameter('arg', ($typeGetter)($arrayWalkArgs[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); + $nativeArrayWalkParameters[] = new DummyParameter('arg', ($nativeTypeGetter)($arrayWalkArgs[2]->value), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null); } $acceptor = $parametersAcceptors[0]; @@ -459,11 +473,12 @@ public static function applyIntrinsicArgOverrides( } } - if ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $arrayFindArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayFindArgVisitor::PARAMETER_NAMES); + if (isset($arrayFindArgs[0]) && (bool) $arrayFindArgs[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); if (isset($parameters[1])) { - $argType = ($typeGetter)($args[0]->value); + $argType = ($typeGetter)($arrayFindArgs[0]->value); $callableType = new CallableType( [ new DummyParameter('value', ($iterableValueTypeGetter)($argType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), @@ -472,7 +487,7 @@ public static function applyIntrinsicArgOverrides( new BooleanType(), false, ); - $nativeArgType = ($nativeTypeGetter)($args[0]->value); + $nativeArgType = ($nativeTypeGetter)($arrayFindArgs[0]->value); $nativeCallableType = new CallableType( [ new DummyParameter('value', ($iterableValueTypeGetter)($nativeArgType), optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null), @@ -486,7 +501,7 @@ public static function applyIntrinsicArgOverrides( } } - $closureBindToVar = $args[0]->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + $closureBindToVar = (ArgumentPositionHelper::getArgsByPosition($args, ClosureBindToVarVisitor::PARAMETER_NAMES)[0] ?? null)?->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); if ( $closureBindToVar instanceof Node\Expr\Variable && is_string($closureBindToVar->name) @@ -532,12 +547,14 @@ public static function applyIntrinsicArgOverrides( } } + $closureBindArg = ArgumentPositionHelper::getArgsByPosition($args, ClosureBindArgVisitor::PARAMETER_NAMES)[0] ?? null; if ( - $args[0]->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null - && $args[0]->value instanceof Node\Expr\Variable - && is_string($args[0]->value->name) + $closureBindArg !== null + && $closureBindArg->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null + && $closureBindArg->value instanceof Node\Expr\Variable + && is_string($closureBindArg->value->name) ) { - $closureVarName = $args[0]->value->name; + $closureVarName = $closureBindArg->value->name; $inFunction = $scope->getFunction(); if ($inFunction !== null) { $closureThisParameters = []; diff --git a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php index 0045da2ad19..b2dfb43393b 100644 --- a/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php +++ b/tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php @@ -3233,6 +3233,81 @@ public function testRoundModePhp84(): void [ 'Parameter #3 $mode of function round expects int<1, 8>|RoundingMode, 0 given.', 12, + ] + ]); + } + + #[RequiresPhp('>= 8.0.0')] + public function testBug15195(): void + { + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/bug-15195.php'], []); + } + + #[RequiresPhp('>= 8.0.0')] + public function testNamedArgumentsOrderIntrinsic(): void + { + $this->analyse([__DIR__ . '/data/named-arguments-order-intrinsic.php'], [ + [ + 'Parameter $callback of function array_filter expects (callable(string): bool)|null, Closure(int): bool given.', + 14, + ], + [ + 'Parameter $callback of function array_filter expects (callable(string): bool)|null, Closure(int): bool given.', + 16, + ], + [ + 'Parameter $callback of function array_filter expects (callable(int, string): bool)|null, Closure(string, int): true given.', + 18, + ], + [ + 'Parameter $callback of function array_map expects (callable(string): mixed)|null, Closure(int): int given.', + 28, + ], + [ + 'Parameter $callback of function array_walk expects callable(string, int<0, max>): mixed, Closure(int, int): void given.', + 37, + ], + [ + 'Parameter $callback of function array_walk expects callable(string, int<0, max>, 1.0): mixed, Closure(string, int, string): void given.', + 39, + ], + [ + 'Parameter $value of function curl_setopt expects 0|2, \'foo\' given.', + 45, + ], + [ + 'Parameter $options of function curl_setopt_array expects array{81: 0|2}, array{81: \'foo\'} given.', + 47, + 'Offset 81 (0|2) does not accept type \'foo\'.', + ], + [ + 'Parameter $array of function implode expects array, string given.', + 56, + ], + ]); + } + + #[RequiresPhp('>= 8.4.0')] + public function testNamedArgumentsOrderArrayFind(): void + { + $this->analyse([__DIR__ . '/data/named-arguments-order-array-find.php'], [ + [ + 'Parameter $callback of function array_find expects callable(string, int<0, max>): bool, Closure(int, int): true given.', + 11, + ], + [ + 'Parameter $callback of function array_find_key expects callable(string, int<0, max>): bool, Closure(int, int): true given.', + 13, + ], + [ + 'Parameter $callback of function array_any expects callable(string, int<0, max>): bool, Closure(int, int): true given.', + 15, + ], + [ + 'Parameter $callback of function array_all expects callable(string, int<0, max>): bool, Closure(int, int): true given.', + 17, ], ]); } diff --git a/tests/PHPStan/Rules/Functions/data/bug-15195.php b/tests/PHPStan/Rules/Functions/data/bug-15195.php new file mode 100644 index 00000000000..17e0c75e9e2 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15195.php @@ -0,0 +1,20 @@ += 8.0 + +namespace Bug15195; + +/** + * @param list $arr + */ +function foo( + array $arr, +): void { + $arr1 = array_filter( + array: $arr, + callback: static fn(string $s): bool => $s !== '', + ); + + $arr2 = array_filter( + callback: static fn(string $s): bool => $s !== '', + array: $arr, + ); +} diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-order-array-find.php b/tests/PHPStan/Rules/Functions/data/named-arguments-order-array-find.php new file mode 100644 index 00000000000..a87522dbc84 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-order-array-find.php @@ -0,0 +1,18 @@ += 8.4 + +namespace NamedArgumentsOrderArrayFind; + +/** + * @param list $arr + */ +function arrayFind(array $arr): void +{ + array_find(callback: static fn (string $v, int $k): bool => true, array: $arr); + array_find(callback: static fn (int $v, int $k): bool => true, array: $arr); + array_find_key(callback: static fn (string $v, int $k): bool => true, array: $arr); + array_find_key(callback: static fn (int $v, int $k): bool => true, array: $arr); + array_any(callback: static fn (string $v, int $k): bool => true, array: $arr); + array_any(callback: static fn (int $v, int $k): bool => true, array: $arr); + array_all(callback: static fn (string $v, int $k): bool => true, array: $arr); + array_all(callback: static fn (int $v, int $k): bool => true, array: $arr); +} diff --git a/tests/PHPStan/Rules/Functions/data/named-arguments-order-intrinsic.php b/tests/PHPStan/Rules/Functions/data/named-arguments-order-intrinsic.php new file mode 100644 index 00000000000..15f4840e9ee --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/named-arguments-order-intrinsic.php @@ -0,0 +1,57 @@ += 8.0 + +namespace NamedArgumentsOrderIntrinsic; + +use CurlHandle; + +/** + * @param list $arr + * @param array $map + */ +function arrayFilter(array $arr, array $map): void +{ + array_filter(callback: static fn (string $s): bool => $s !== '', array: $arr); + array_filter(callback: static fn (int $i): bool => $i !== 0, array: $arr); + array_filter(mode: ARRAY_FILTER_USE_KEY, callback: static fn (string $k): bool => $k !== '', array: $map); + array_filter(mode: ARRAY_FILTER_USE_KEY, callback: static fn (int $k): bool => $k !== 0, array: $map); + array_filter(mode: ARRAY_FILTER_USE_BOTH, callback: static fn (int $v, string $k): bool => true, array: $map); + array_filter(mode: ARRAY_FILTER_USE_BOTH, callback: static fn (string $v, int $k): bool => true, array: $map); +} + +/** + * @param list $arr + */ +function arrayMap(array $arr): void +{ + array_map(callback: static fn (string $s): string => $s, array: $arr); + array_map(array: $arr, callback: static fn (string $s): string => $s); + array_map(array: $arr, callback: static fn (int $i): int => $i); +} + +/** + * @param list $arr + */ +function arrayWalk(array $arr): void +{ + array_walk(callback: static function (string $v, int $k): void {}, array: $arr); + array_walk(callback: static function (int $v, int $k): void {}, array: $arr); + array_walk(arg: 1.0, callback: static function (string $v, int $k, float $a): void {}, array: $arr); + array_walk(arg: 1.0, callback: static function (string $v, int $k, string $a): void {}, array: $arr); +} + +function curl(CurlHandle $ch): void +{ + curl_setopt(value: 2, option: CURLOPT_SSL_VERIFYHOST, handle: $ch); + curl_setopt(value: 'foo', option: CURLOPT_SSL_VERIFYHOST, handle: $ch); + curl_setopt_array(options: [CURLOPT_SSL_VERIFYHOST => 2], handle: $ch); + curl_setopt_array(options: [CURLOPT_SSL_VERIFYHOST => 'foo'], handle: $ch); +} + +/** + * @param list $arr + */ +function implodes(array $arr): void +{ + implode(array: $arr, separator: ','); + implode(array: 'foo', separator: ','); +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 328900e183e..0764d1e70e5 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -3473,6 +3473,25 @@ public function testClosureBindToParamClosureThis(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testClosureBindToParamClosureThisNamedArgs(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->checkExplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-to-param-closure-this-named-args.php'], [ + [ + 'Parameter $newThis of method Closure::bindTo() expects stdClass, ClosureBindToParamClosureThisNamedArgs\Foo given.', + 14, + ], + [ + 'Parameter $newThis of method Closure::bindTo() expects stdClass, ClosureBindToParamClosureThisNamedArgs\Foo given.', + 16, + ], + ]); + } + public function testBug11010(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php index fbed93ba6c9..3429044a452 100644 --- a/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php @@ -847,6 +847,24 @@ public function testClosureBindParamClosureThis(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testClosureBindParamClosureThisNamedArgs(): void + { + $this->checkThisOnly = false; + $this->checkExplicitMixed = true; + $this->checkImplicitMixed = true; + $this->analyse([__DIR__ . '/data/closure-bind-param-closure-this-named-args.php'], [ + [ + 'Parameter $newThis of static method Closure::bind() expects stdClass, ClosureBindParamClosureThisNamedArgs\Foo given.', + 16, + ], + [ + 'Parameter $newThis of static method Closure::bind() expects stdClass, ClosureBindParamClosureThisNamedArgs\Foo given.', + 18, + ], + ]); + } + public function testClosureBind(): void { $this->checkThisOnly = false; diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this-named-args.php b/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this-named-args.php new file mode 100644 index 00000000000..811a8947d27 --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/closure-bind-param-closure-this-named-args.php @@ -0,0 +1,21 @@ += 8.0 + +namespace ClosureBindParamClosureThisNamedArgs; + +use Closure; + +class Foo +{ + + /** + * @param-closure-this \stdClass $c + */ + public function doFoo(\Closure $c): void + { + Closure::bind(closure: $c, newThis: new \stdClass()); // ok + Closure::bind(closure: $c, newThis: new self()); // error + Closure::bind(newThis: new \stdClass(), closure: $c); // ok + Closure::bind(newThis: new self(), closure: $c); // error + } + +} diff --git a/tests/PHPStan/Rules/Methods/data/closure-bind-to-param-closure-this-named-args.php b/tests/PHPStan/Rules/Methods/data/closure-bind-to-param-closure-this-named-args.php new file mode 100644 index 00000000000..0ed13fd2b2a --- /dev/null +++ b/tests/PHPStan/Rules/Methods/data/closure-bind-to-param-closure-this-named-args.php @@ -0,0 +1,19 @@ += 8.0 + +namespace ClosureBindToParamClosureThisNamedArgs; + +class Foo +{ + + /** + * @param-closure-this \stdClass $c + */ + public function doFoo(\Closure $c): void + { + $c->bindTo(newThis: new \stdClass()); // ok + $c->bindTo(newThis: new self()); // error + $c->bindTo(newScope: null, newThis: new \stdClass()); // ok + $c->bindTo(newScope: null, newThis: new self()); // error + } + +} From 6b31bdbd45c51aa6faa0ad043f04929bc70edd03 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 20 Sep 2026 20:07:38 +0000 Subject: [PATCH 2/3] Move getArgsByPosition() into ArgumentsNormalizer 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 --- src/Analyser/ArgumentsNormalizer.php | 66 ++++++++++++++++--- src/Parser/ArgumentPositionHelper.php | 65 ------------------ src/Parser/ArrayFilterArgVisitor.php | 3 +- src/Parser/ArrayFindArgVisitor.php | 3 +- src/Parser/ArrayMapArgVisitor.php | 3 +- src/Parser/ArrayWalkArgVisitor.php | 3 +- src/Parser/ClosureBindArgVisitor.php | 3 +- src/Parser/ClosureBindToVarVisitor.php | 3 +- src/Parser/CurlSetOptArgVisitor.php | 3 +- src/Parser/CurlSetOptArrayArgVisitor.php | 3 +- src/Parser/ImplodeArgVisitor.php | 3 +- src/Reflection/ParametersAcceptorSelector.php | 19 +++--- 12 files changed, 85 insertions(+), 92 deletions(-) delete mode 100644 src/Parser/ArgumentPositionHelper.php diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index 7d5cf5718f8..da83bf2508d 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -23,6 +23,7 @@ use function array_is_list; use function array_key_exists; use function array_keys; +use function array_search; use function array_values; use function count; use function is_string; @@ -303,14 +304,7 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array return []; } - $hasNamedArgs = false; - foreach ($callArgs as $arg) { - if ($arg->name !== null) { - $hasNamedArgs = true; - break; - } - } - if (!$hasNamedArgs) { + if (!self::hasNamedArgs($callArgs)) { return array_values($callArgs); } @@ -443,6 +437,62 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array return $reorderedArgs; } + /** + * Maps the arguments of a call onto the positions of the callee's parameters, + * leaving out named arguments that don't match any of $parameterNames. + * + * Unlike reorderArgs() this doesn't need a ParametersAcceptor - the caller + * spells out the parameter names it knows about, so it also works in the + * parser visitors, which run before any reflection is available. It also + * returns the original Arg objects instead of copies, which is what makes + * the attributes those visitors set visible on the analysed AST. + * + * @internal + * @param Arg[] $args + * @param list $parameterNames parameter names in signature order + * @return array + */ + public static function getArgsByPosition(array $args, array $parameterNames): array + { + if (!self::hasNamedArgs($args)) { + return $args; + } + + $argsByPosition = []; + foreach ($args as $i => $arg) { + if ($arg->name === null) { + // positional arguments always precede named ones + $argsByPosition[$i] = $arg; + continue; + } + + $position = array_search($arg->name->toString(), $parameterNames, true); + if ($position === false) { + continue; + } + + $argsByPosition[$position] = $arg; + } + + return $argsByPosition; + } + + /** + * @param Arg[] $args + */ + private static function hasNamedArgs(array $args): bool + { + foreach ($args as $arg) { + if ($arg->name === null) { + continue; + } + + return true; + } + + return false; + } + /** * The printed form of an expression is derived from its own arguments, so * it must not travel to a node whose arguments were just reordered — the diff --git a/src/Parser/ArgumentPositionHelper.php b/src/Parser/ArgumentPositionHelper.php deleted file mode 100644 index aaf047bd18e..00000000000 --- a/src/Parser/ArgumentPositionHelper.php +++ /dev/null @@ -1,65 +0,0 @@ - $parameterNames parameter names in signature order - * @return array - */ - public static function getArgsByPosition(array $args, array $parameterNames): array - { - $hasNamedArgs = false; - foreach ($args as $arg) { - if ($arg->name === null) { - continue; - } - - $hasNamedArgs = true; - break; - } - - if (!$hasNamedArgs) { - return $args; - } - - $argsByPosition = []; - foreach ($args as $i => $arg) { - if ($arg->name === null) { - // positional arguments always precede named ones - $argsByPosition[$i] = $arg; - continue; - } - - $position = array_search($arg->name->toString(), $parameterNames, true); - if ($position === false) { - continue; - } - - $argsByPosition[$position] = $arg; - } - - return $argsByPosition; - } - -} diff --git a/src/Parser/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php index 83c72f0d27e..560341a378a 100644 --- a/src/Parser/ArrayFilterArgVisitor.php +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; @@ -23,7 +24,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_filter') { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php index b039fbf5af9..99dc6af6400 100644 --- a/src/Parser/ArrayFindArgVisitor.php +++ b/src/Parser/ArrayFindArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; use function in_array; @@ -24,7 +25,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php index 4001abd2bdd..119ca41cf14 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; use function array_slice; @@ -26,7 +27,7 @@ public function enterNode(Node $node): ?Node $functionName = $node->name->toLowerString(); if ($functionName === 'array_map') { $args = $node->getArgs(); - $callbackArg = ArgumentPositionHelper::getArgsByPosition($args, self::PARAMETER_NAMES)[0] ?? null; + $callbackArg = ArgumentsNormalizer::getArgsByPosition($args, self::PARAMETER_NAMES)[0] ?? null; if ($callbackArg === null) { return null; } diff --git a/src/Parser/ArrayWalkArgVisitor.php b/src/Parser/ArrayWalkArgVisitor.php index cdb72f916d6..5c1a627e5bc 100644 --- a/src/Parser/ArrayWalkArgVisitor.php +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; @@ -23,7 +24,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_walk') { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php index 1df3da871ef..df45f377236 100644 --- a/src/Parser/ClosureBindArgVisitor.php +++ b/src/Parser/ClosureBindArgVisitor.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; use function count; @@ -32,7 +33,7 @@ public function enterNode(Node $node): ?Node ) { $args = $node->getArgs(); if (count($args) > 1) { - $args = ArgumentPositionHelper::getArgsByPosition($args, self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($args, self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php index b429a6a4780..08974133342 100644 --- a/src/Parser/ClosureBindToVarVisitor.php +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Identifier; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; @@ -27,7 +28,7 @@ public function enterNode(Node $node): ?Node && $node->name->toLowerString() === 'bindto' && !$node->isFirstClassCallable() ) { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, $node->var); } diff --git a/src/Parser/CurlSetOptArgVisitor.php b/src/Parser/CurlSetOptArgVisitor.php index 58784d582b2..07bd8cc34e1 100644 --- a/src/Parser/CurlSetOptArgVisitor.php +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; @@ -23,7 +24,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt') { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[0])) { $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/CurlSetOptArrayArgVisitor.php b/src/Parser/CurlSetOptArrayArgVisitor.php index 4338b34ec93..c9a7ee71a87 100644 --- a/src/Parser/CurlSetOptArrayArgVisitor.php +++ b/src/Parser/CurlSetOptArrayArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; @@ -23,7 +24,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt_array') { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); if (isset($args[1])) { $args[1]->setAttribute(self::ATTRIBUTE_NAME, true); } diff --git a/src/Parser/ImplodeArgVisitor.php b/src/Parser/ImplodeArgVisitor.php index 6de4f0343c4..0b41edea998 100644 --- a/src/Parser/ImplodeArgVisitor.php +++ b/src/Parser/ImplodeArgVisitor.php @@ -5,6 +5,7 @@ use Override; use PhpParser\Node; use PhpParser\NodeVisitorAbstract; +use PHPStan\Analyser\ArgumentsNormalizer; use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Turbo\ShadowedByTurboExtension; use function in_array; @@ -24,7 +25,7 @@ public function enterNode(Node $node): ?Node if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['implode', 'join'], true)) { - $args = ArgumentPositionHelper::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); + $args = ArgumentsNormalizer::getArgsByPosition($node->getArgs(), self::PARAMETER_NAMES); // implode(array: $a) leaves the first parameter unfilled $markedArg = $args[0] ?? $args[1] ?? null; if ($markedArg !== null) { diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index 967b9241c4d..0342e2cc70f 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -9,7 +9,6 @@ use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\Scope; use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr; -use PHPStan\Parser\ArgumentPositionHelper; use PHPStan\Parser\ArrayFilterArgVisitor; use PHPStan\Parser\ArrayFindArgVisitor; use PHPStan\Parser\ArrayMapArgVisitor; @@ -205,7 +204,7 @@ public static function applyIntrinsicArgOverrides( count($args) > 0 && count($parametersAcceptors) > 0 ) { - $arrayMapArgs = (ArgumentPositionHelper::getArgsByPosition($args, ArrayMapArgVisitor::PARAMETER_NAMES)[0] ?? null)?->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $arrayMapArgs = (ArgumentsNormalizer::getArgsByPosition($args, ArrayMapArgVisitor::PARAMETER_NAMES)[0] ?? null)?->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callbackParameters = []; $nativeCallbackParameters = []; @@ -253,7 +252,7 @@ public static function applyIntrinsicArgOverrides( } } - $curlSetOptArgs = ArgumentPositionHelper::getArgsByPosition($args, CurlSetOptArgVisitor::PARAMETER_NAMES); + $curlSetOptArgs = ArgumentsNormalizer::getArgsByPosition($args, CurlSetOptArgVisitor::PARAMETER_NAMES); if ( count($args) >= 3 && isset($curlSetOptArgs[0], $curlSetOptArgs[1]) @@ -302,7 +301,7 @@ public static function applyIntrinsicArgOverrides( } } - $curlSetOptArrayArgs = ArgumentPositionHelper::getArgsByPosition($args, CurlSetOptArrayArgVisitor::PARAMETER_NAMES); + $curlSetOptArrayArgs = ArgumentsNormalizer::getArgsByPosition($args, CurlSetOptArrayArgVisitor::PARAMETER_NAMES); if ( isset($curlSetOptArrayArgs[1]) && (bool) $curlSetOptArrayArgs[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME) @@ -358,7 +357,7 @@ public static function applyIntrinsicArgOverrides( } } - $arrayFilterArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayFilterArgVisitor::PARAMETER_NAMES); + $arrayFilterArgs = ArgumentsNormalizer::getArgsByPosition($args, ArrayFilterArgVisitor::PARAMETER_NAMES); if (isset($arrayFilterArgs[0]) && (bool) $arrayFilterArgs[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { $arrayFilterParameters = null; $nativeArrayFilterParameters = null; @@ -415,7 +414,7 @@ public static function applyIntrinsicArgOverrides( } } - $implodeArgs = ArgumentPositionHelper::getArgsByPosition($args, ImplodeArgVisitor::PARAMETER_NAMES); + $implodeArgs = ArgumentsNormalizer::getArgsByPosition($args, ImplodeArgVisitor::PARAMETER_NAMES); $implodeMarkedArg = $implodeArgs[0] ?? $implodeArgs[1] ?? null; if (count($args) <= 2 && $implodeMarkedArg !== null && (bool) $implodeMarkedArg->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { $acceptor = $namedArgumentsVariants[0] ?? $parametersAcceptors[0]; @@ -446,7 +445,7 @@ public static function applyIntrinsicArgOverrides( ]; } - $arrayWalkArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayWalkArgVisitor::PARAMETER_NAMES); + $arrayWalkArgs = ArgumentsNormalizer::getArgsByPosition($args, ArrayWalkArgVisitor::PARAMETER_NAMES); if (isset($arrayWalkArgs[0]) && (bool) $arrayWalkArgs[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { $arrayArgType = ($typeGetter)($arrayWalkArgs[0]->value); $nativeArrayArgType = ($nativeTypeGetter)($arrayWalkArgs[0]->value); @@ -473,7 +472,7 @@ public static function applyIntrinsicArgOverrides( } } - $arrayFindArgs = ArgumentPositionHelper::getArgsByPosition($args, ArrayFindArgVisitor::PARAMETER_NAMES); + $arrayFindArgs = ArgumentsNormalizer::getArgsByPosition($args, ArrayFindArgVisitor::PARAMETER_NAMES); if (isset($arrayFindArgs[0]) && (bool) $arrayFindArgs[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { $acceptor = $parametersAcceptors[0]; $parameters = $acceptor->getParameters(); @@ -501,7 +500,7 @@ public static function applyIntrinsicArgOverrides( } } - $closureBindToVar = (ArgumentPositionHelper::getArgsByPosition($args, ClosureBindToVarVisitor::PARAMETER_NAMES)[0] ?? null)?->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); + $closureBindToVar = (ArgumentsNormalizer::getArgsByPosition($args, ClosureBindToVarVisitor::PARAMETER_NAMES)[0] ?? null)?->getAttribute(ClosureBindToVarVisitor::ATTRIBUTE_NAME); if ( $closureBindToVar instanceof Node\Expr\Variable && is_string($closureBindToVar->name) @@ -547,7 +546,7 @@ public static function applyIntrinsicArgOverrides( } } - $closureBindArg = ArgumentPositionHelper::getArgsByPosition($args, ClosureBindArgVisitor::PARAMETER_NAMES)[0] ?? null; + $closureBindArg = ArgumentsNormalizer::getArgsByPosition($args, ClosureBindArgVisitor::PARAMETER_NAMES)[0] ?? null; if ( $closureBindArg !== null && $closureBindArg->getAttribute(ClosureBindArgVisitor::ATTRIBUTE_NAME) !== null From 4b7efa49851957f206618ef652b6d204310201ea Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 20 Sep 2026 20:48:19 +0000 Subject: [PATCH 3/3] Make ArgumentsNormalizer::getArgsByPosition() part of the public API 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 --- src/Analyser/ArgumentsNormalizer.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index da83bf2508d..e0028ce1d84 100644 --- a/src/Analyser/ArgumentsNormalizer.php +++ b/src/Analyser/ArgumentsNormalizer.php @@ -447,7 +447,6 @@ public static function reorderArgs(ParametersAcceptor $parametersAcceptor, array * returns the original Arg objects instead of copies, which is what makes * the attributes those visitors set visible on the analysed AST. * - * @internal * @param Arg[] $args * @param list $parameterNames parameter names in signature order * @return array