Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Fixture;

use Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Source\SomeCacheInterface;

class SkipCallableParamUnion {
public function __construct(
private readonly SomeCacheInterface $cache,
) {
}

public function get(): int {
return $this->cache->get('bar', fn() => time());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Source;

interface SomeCacheInterface {
/**
* @template T
*
* @param string $key
* @param (callable(CacheItemInterface,bool):T)|(callable(ItemInterface,bool):T)|CallbackInterface<T> $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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Rector\CodingStyle\Rector\FunctionLike;

use PhpParser\Comment\Doc;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
Expand All @@ -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;
Expand Down Expand Up @@ -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
) {
}

Expand Down Expand Up @@ -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;
}
}
}
Expand Down Expand Up @@ -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,
Expand Down