diff --git a/src/Analyser/ArgumentsNormalizer.php b/src/Analyser/ArgumentsNormalizer.php index 7d5cf5718f8..e0028ce1d84 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,61 @@ 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. + * + * @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/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php index 09b11ff3ad6..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; @@ -15,13 +16,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 = 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 ca52b483b72..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; @@ -16,13 +17,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 = 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 b0b35180d63..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; @@ -17,6 +18,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 +27,22 @@ public function enterNode(Node $node): ?Node $functionName = $node->name->toLowerString(); if ($functionName === 'array_map') { $args = $node->getArgs(); + $callbackArg = ArgumentsNormalizer::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..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; @@ -15,13 +16,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 = 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 93c22b7c615..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; @@ -17,6 +18,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 +33,10 @@ public function enterNode(Node $node): ?Node ) { $args = $node->getArgs(); if (count($args) > 1) { - $args[0]->setAttribute(self::ATTRIBUTE_NAME, true); + $args = ArgumentsNormalizer::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..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; @@ -16,6 +17,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 +28,7 @@ public function enterNode(Node $node): ?Node && $node->name->toLowerString() === 'bindto' && !$node->isFirstClassCallable() ) { - $args = $node->getArgs(); + $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 748adc8b14e..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; @@ -15,13 +16,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 = 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 694fc983632..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; @@ -15,13 +16,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 = 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 ed3008f6c13..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; @@ -16,15 +17,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 = ArgumentsNormalizer::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..0342e2cc70f 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -204,7 +204,7 @@ public static function applyIntrinsicArgOverrides( count($args) > 0 && count($parametersAcceptors) > 0 ) { - $arrayMapArgs = $args[0]->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); + $arrayMapArgs = (ArgumentsNormalizer::getArgsByPosition($args, ArrayMapArgVisitor::PARAMETER_NAMES)[0] ?? null)?->value->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME); if ($arrayMapArgs !== null) { $callbackParameters = []; $nativeCallbackParameters = []; @@ -252,8 +252,13 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) >= 3 && (bool) $args[0]->getAttribute(CurlSetOptArgVisitor::ATTRIBUTE_NAME)) { - $optType = ($typeGetter)($args[1]->value); + $curlSetOptArgs = ArgumentsNormalizer::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 +301,12 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) >= 2 && (bool) $args[1]->getAttribute(CurlSetOptArrayArgVisitor::ATTRIBUTE_NAME)) { - $optArrayType = ($typeGetter)($args[1]->value); + $curlSetOptArrayArgs = ArgumentsNormalizer::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 +357,28 @@ public static function applyIntrinsicArgOverrides( } } - if ((bool) $args[0]->getAttribute(ArrayFilterArgVisitor::ATTRIBUTE_NAME)) { + $arrayFilterArgs = ArgumentsNormalizer::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 +387,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 +398,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 +414,13 @@ public static function applyIntrinsicArgOverrides( } } - if (count($args) <= 2 && (bool) $args[0]->getAttribute(ImplodeArgVisitor::ATTRIBUTE_NAME)) { + $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]; $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 +445,10 @@ public static function applyIntrinsicArgOverrides( ]; } - if ((bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { - $arrayArgType = ($typeGetter)($args[0]->value); - $nativeArrayArgType = ($nativeTypeGetter)($args[0]->value); + $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); $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 +457,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 +472,12 @@ public static function applyIntrinsicArgOverrides( } } - if ((bool) $args[0]->getAttribute(ArrayFindArgVisitor::ATTRIBUTE_NAME)) { + $arrayFindArgs = ArgumentsNormalizer::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 +486,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 +500,7 @@ public static function applyIntrinsicArgOverrides( } } - $closureBindToVar = $args[0]->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) @@ -532,12 +546,14 @@ public static function applyIntrinsicArgOverrides( } } + $closureBindArg = ArgumentsNormalizer::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 + } + +}