diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_union.php.inc b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_union.php.inc new file mode 100644 index 00000000000..5d3b3e485ce --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_union.php.inc @@ -0,0 +1,16 @@ +cache->get('bar', fn() => time()); + } +} diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Source/SomeCacheInterface.php b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Source/SomeCacheInterface.php new file mode 100644 index 00000000000..19a19a281e2 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Source/SomeCacheInterface.php @@ -0,0 +1,19 @@ + $callback + * @param float|null $beta + * @param array &$metadata + * + * @return T + * + * @throws InvalidArgumentException When $key is not valid or when $beta is negative + */ + public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed; +} diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index 2842325507d..b36f845b926 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -4,6 +4,7 @@ namespace Rector\CodingStyle\Rector\FunctionLike; +use PhpParser\Comment\Doc; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; @@ -23,10 +24,16 @@ use PhpParser\Node\VariadicPlaceholder; use PhpParser\NodeVisitor; use PHPStan\Analyser\Scope; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\Reflection\Annotations\AnnotationMethodReflection; +use PHPStan\Reflection\ExtendedFunctionVariant; +use PHPStan\Reflection\Native\NativeFunctionReflection; +use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Reflection\ResolvedFunctionVariantWithOriginal; use PHPStan\Type\CallableType; use PHPStan\Type\ObjectType; +use PHPStan\Type\UnionType; +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PhpParser\AstResolver; use Rector\PhpParser\Node\BetterNodeFinder; @@ -56,7 +63,8 @@ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implem public function __construct( private readonly AstResolver $astResolver, private readonly ReflectionResolver $reflectionResolver, - private readonly BetterNodeFinder $betterNodeFinder + private readonly BetterNodeFinder $betterNodeFinder, + private readonly PhpDocInfoFactory $phpDocInfoFactory ) { } @@ -127,69 +135,80 @@ public function refactor(Node $node): null|CallLike ); if ($reflection instanceof ResolvedFunctionVariantWithOriginal) { - return null; + $reflection = ParametersAcceptorSelector::combineAcceptors( + $methodReflection->getVariants() + ); + + if (! $reflection instanceof ExtendedFunctionVariant) { + return null; + } } $classMethodOrFunction = $this->astResolver->resolveClassMethodOrFunctionFromCall($node); + if (! $classMethodOrFunction instanceof FunctionLike) { + return null; + } + foreach ($reflection->getParameters() as $index => $parameterReflection) { if ($index !== $key) { continue; } if ($parameterReflection->getType() instanceof CallableType - && count($parameterReflection->getType()->getParameters()) > 1 + && + count($parameterReflection->getType()->getParameters()) !== 1 + && ! $methodReflection instanceof NativeFunctionReflection + && $this->hasDocCommentForCallable($classMethodOrFunction, $index) ) { $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); return null; } - if ($classMethodOrFunction instanceof FunctionLike) { - $parameterName = $parameterReflection->getName(); + $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; - } + $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 + ); - $isClosureBindTo = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( - $classMethodOrFunction, - function (Node $node) use ($parameterName): bool { - if (! $node instanceof MethodCall) { - return false; - } + if ($isInvokable) { + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); + return null; + } - if (! $node->name instanceof Identifier) { - return false; - } + $isClosureBindTo = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( + $classMethodOrFunction, + function (Node $node) use ($parameterName): bool { + if (! $node instanceof MethodCall) { + return false; + } - if (! $this->isName($node->name, 'bindTo')) { - return false; - } + if (! $node->name instanceof Identifier) { + return false; + } - if (! $node->var instanceof Variable) { - return false; - } + if (! $this->isName($node->name, 'bindTo')) { + return false; + } - if (! $this->isObjectType($node->var, new ObjectType('Closure'))) { - return false; - } + if (! $node->var instanceof Variable) { + return false; + } - return $this->isName($node->var, $parameterName); + if (! $this->isObjectType($node->var, new ObjectType('Closure'))) { + return false; } - ); - if ($isClosureBindTo) { - $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); - return null; + return $this->isName($node->var, $parameterName); } + ); + + if ($isClosureBindTo) { + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); + return null; } } } @@ -218,6 +237,37 @@ public function provideMinPhpVersion(): int return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; } + private function hasDocCommentForCallable(FunctionLike $functionLike, int $index): bool + { + $docComment = $functionLike->getDocComment(); + if (! $docComment instanceof Doc) { + return false; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($functionLike); + $params = $functionLike->getParams(); + + $paramName = null; + foreach ($params as $key => $param) { + if ($key === $index) { + $paramName = (string) $this->getName($param); + break; + } + } + + if ($paramName === null) { + return false; + } + + $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName); + if ($paramTagValueNode instanceof ParamTagValueNode) { + $type = $phpDocInfo->getParamType($paramName); + return ($type instanceof CallableType && count($type->getParameters()) !== 1) || $type instanceof UnionType; + } + + return false; + } + private function shouldSkip( ArrowFunction|Closure $node, FuncCall|MethodCall|StaticCall $callLike,