diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable.php.inc b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable.php.inc new file mode 100644 index 00000000000..381d53d883a --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable.php.inc @@ -0,0 +1,14 @@ +name, $this->object); + } +} + +$obj = new SkipParamUsedAsInvokable('name', new \stdClass()); +$obj->render(fn(string $label, object $object): string => ucfirst($label)); diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable_from_new.php.inc b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable_from_new.php.inc new file mode 100644 index 00000000000..6ae020856a4 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_param_used_as_invokable_from_new.php.inc @@ -0,0 +1,15 @@ +name, $this->object); + } +} + +$obj = new SkipParamUsedAsInvokableFromNew('name', new \stdClass(), fn(string $label, object $object): string => ucfirst($label)); diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index 9041cc32385..6a218786982 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -27,6 +27,7 @@ use PHPStan\Type\CallableType; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PhpParser\AstResolver; +use Rector\PhpParser\Node\BetterNodeFinder; use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; use Rector\Reflection\ReflectionResolver; @@ -47,7 +48,8 @@ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implem public function __construct( private readonly AstResolver $astResolver, - private readonly ReflectionResolver $reflectionResolver + private readonly ReflectionResolver $reflectionResolver, + private readonly BetterNodeFinder $betterNodeFinder ) { } @@ -112,14 +114,35 @@ public function refactor(Node $node): null|CallLike return null; } + $classMethodOrFunction = $this->astResolver->resolveClassMethodOrFunctionFromCall($node); foreach ($reflection->getParameters() as $index => $parameterReflection) { - if ($index === $key - && $parameterReflection->getType() instanceof CallableType + if ($index !== $key) { + continue; + } + + if ($parameterReflection->getType() instanceof CallableType && count($parameterReflection->getType()->getParameters()) > 1 ) { $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); return null; } + + if ($classMethodOrFunction instanceof FunctionLike) { + $parameterName = $parameterReflection->getName(); + + $isInvokable = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( + $classMethodOrFunction, + fn (Node $node): bool => $node instanceof FuncCall + && $node->name instanceof Variable + && $this->isName($node->name, $parameterName) + && count($node->args) > 1 + ); + + if ($isInvokable) { + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); + return null; + } + } } } } diff --git a/src/PhpParser/AstResolver.php b/src/PhpParser/AstResolver.php index 0e85c338d3e..392b22b2f29 100644 --- a/src/PhpParser/AstResolver.php +++ b/src/PhpParser/AstResolver.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\NullsafeMethodCall; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Name; @@ -111,7 +112,7 @@ function (Node $node) use ($classLikeName, $methodName, &$classMethod): bool { } public function resolveClassMethodOrFunctionFromCall( - FuncCall | StaticCall | MethodCall $call + FuncCall | StaticCall | MethodCall | New_ $call ): ClassMethod | Function_ | null { if ($call instanceof FuncCall) { return $this->resolveFunctionFromFuncCall($call); @@ -165,8 +166,21 @@ public function resolveClassMethod(string $className, string $methodName): ?Clas return $classMethod; } - public function resolveClassMethodFromCall(MethodCall | StaticCall | NullsafeMethodCall $call): ?ClassMethod + public function resolveClassMethodFromCall(MethodCall | StaticCall | NullsafeMethodCall | New_ $call): ?ClassMethod { + if ($call instanceof New_) { + if ($call->class instanceof Class_) { + return null; + } + + $className = $this->nodeNameResolver->getName($call->class); + if ($className === null) { + return null; + } + + return $this->resolveClassMethod($className, MethodName::CONSTRUCT); + } + $callerStaticType = ($call instanceof MethodCall || $call instanceof NullsafeMethodCall) ? $this->nodeTypeResolver->getType($call->var) : $this->nodeTypeResolver->getType($call->class);