diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/callable_param.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/callable_param.php.inc new file mode 100644 index 00000000000..5675fa26d92 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/callable_param.php.inc @@ -0,0 +1,39 @@ +withCallable([$this, 'name']); + } + + public function name() + { + } +} + +?> +----- +withCallable($this->name(...)); + } + + public function name() + { + } +} + +?> diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_array_param.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_array_param.php.inc new file mode 100644 index 00000000000..0e1fc4a041a --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_array_param.php.inc @@ -0,0 +1,19 @@ +withArray([$this, 'name']); + + SomeObjectWithArrayArgument::staticWithArray([$this, 'name']); + } + + public function name() + { + } +} diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_union_array_param.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_union_array_param.php.inc new file mode 100644 index 00000000000..f7b5d055e26 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/skip_union_array_param.php.inc @@ -0,0 +1,17 @@ +getAttribute(AttributeKey::IS_ARG_NOT_ACCEPTING_CLOSURE)) { + return null; + } + if ($node->getAttribute(AttributeKey::IS_INSIDE_SYMFONY_PHP_CLOSURE)) { return null; } diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index cad07eb5d1d..b45275fcf17 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -117,6 +117,7 @@ use Rector\PhpParser\Node\NodeFactory; use Rector\PhpParser\NodeTraverser\RectorNodeTraverser; use Rector\PhpParser\NodeVisitor\ArgNodeVisitor; +use Rector\PhpParser\NodeVisitor\ArgNotAcceptingClosureNodeVisitor; use Rector\PhpParser\NodeVisitor\AssignedToNodeVisitor; use Rector\PhpParser\NodeVisitor\ByRefReturnNodeVisitor; use Rector\PhpParser\NodeVisitor\ByRefVariableNodeVisitor; @@ -250,6 +251,7 @@ final class LazyContainerFactory ParamDefaultNodeVisitor::class, ClassConstFetchNodeVisitor::class, CallLikeThisBoundClosureArgsNodeVisitor::class, + ArgNotAcceptingClosureNodeVisitor::class, ]; /** diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 33f40638ebe..6be90a114d5 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -176,4 +176,10 @@ final class AttributeKey public const string IS_IN_TRY_BLOCK = 'is_in_try_block'; public const string NEWLINE_ON_FLUENT_CALL = 'newline_on_fluent_call'; + + /** + * The arg value is passed to a parameter whose type does not accept a Closure, + * e.g. an array callable passed to a "string|array|null" parameter + */ + public const string IS_ARG_NOT_ACCEPTING_CLOSURE = 'is_arg_not_accepting_closure'; } diff --git a/src/PhpParser/NodeVisitor/ArgNotAcceptingClosureNodeVisitor.php b/src/PhpParser/NodeVisitor/ArgNotAcceptingClosureNodeVisitor.php new file mode 100644 index 00000000000..155923c4ec1 --- /dev/null +++ b/src/PhpParser/NodeVisitor/ArgNotAcceptingClosureNodeVisitor.php @@ -0,0 +1,115 @@ +isFirstClassCallable()) { + return null; + } + + $args = $node->getArgs(); + if (! array_any($args, static fn (Arg $arg): bool => $arg->value instanceof Array_)) { + return null; + } + + $functionLikeReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + if (! $functionLikeReflection instanceof FunctionReflection && ! $functionLikeReflection instanceof MethodReflection) { + return null; + } + + $parameterReflections = ParametersAcceptorSelectorVariantsWrapper::select( + $functionLikeReflection, + $node, + ScopeFetcher::fetch($node) + )->getParameters(); + + $closureObjectType = new ObjectType('Closure'); + + foreach ($args as $position => $arg) { + if (! $arg->value instanceof Array_) { + continue; + } + + $parameterReflection = $this->matchParameterReflection($arg, $position, $parameterReflections); + if (! $parameterReflection instanceof ParameterReflection) { + continue; + } + + if (! $parameterReflection->getType()->accepts($closureObjectType, true)->no()) { + continue; + } + + $arg->value->setAttribute(AttributeKey::IS_ARG_NOT_ACCEPTING_CLOSURE, true); + } + + return $node; + } + + /** + * @param ParameterReflection[] $parameterReflections + */ + private function matchParameterReflection( + Arg $arg, + int $position, + array $parameterReflections + ): ?ParameterReflection { + if ($arg->name instanceof Identifier) { + $argName = $arg->name->toString(); + + foreach ($parameterReflections as $parameterReflection) { + if ($parameterReflection->getName() === $argName) { + return $parameterReflection; + } + } + + return null; + } + + if (isset($parameterReflections[$position])) { + return $parameterReflections[$position]; + } + + $lastParameterReflection = end($parameterReflections); + if ($lastParameterReflection instanceof ParameterReflection && $lastParameterReflection->isVariadic()) { + return $lastParameterReflection; + } + + return null; + } +}