From 30a38c69561790164e916013ab0eb44df24db0f0 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 22 Oct 2025 21:48:00 +0700 Subject: [PATCH 1/4] [Php81][CodingStyle] Skip callable param assign with signature multi params on FunctionLikeToFirstClassCallableRector --- ...assign_with_signature_multi_params.php.inc | 29 ++++++++ ...FunctionLikeToFirstClassCallableRector.php | 69 ++++++++++++++++++- 2 files changed, 95 insertions(+), 3 deletions(-) create mode 100644 rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_assign_with_signature_multi_params.php.inc diff --git a/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_assign_with_signature_multi_params.php.inc b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_assign_with_signature_multi_params.php.inc new file mode 100644 index 00000000000..d6cad61df0c --- /dev/null +++ b/rules-tests/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector/Fixture/skip_callable_param_assign_with_signature_multi_params.php.inc @@ -0,0 +1,29 @@ +transform = $callable; + return $this; + } + + public function run($value, $optionalData = []) + { + var_dump(($this->transform)($value, $optionalData)); + } +} + +(new SkipCallableParamAssignWithSignatureMultiParams()) + ->withTransform(fn (string $datum): string => trim($datum)) + ->run(' some data '); + + +?> diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index 64c261879cd..99b480ca167 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -12,6 +12,7 @@ use PhpParser\Node\Expr\Closure; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\FunctionLike; use PhpParser\Node\Identifier; @@ -20,9 +21,12 @@ use PhpParser\Node\VariadicPlaceholder; use PhpParser\NodeVisitor; use PHPStan\Analyser\Scope; +use PHPStan\Reflection\ResolvedFunctionVariantWithOriginal; +use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PhpParser\AstResolver; use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; +use Rector\Reflection\ReflectionResolver; use Rector\ValueObject\PhpVersionFeature; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; @@ -33,8 +37,14 @@ */ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implements MinPhpVersionInterface { + /** + * @var string + */ + private const SKIP_BASED_ON_CALLBACK_SIGNATURE = 'skip_based_on_callback_signature'; + public function __construct( - private readonly AstResolver $astResolver + private readonly AstResolver $astResolver, + private readonly ReflectionResolver $reflectionResolver ) { } @@ -59,14 +69,63 @@ function ($parameter) { public function getNodeTypes(): array { - return [ArrowFunction::class, Closure::class]; + return [ + MethodCall::class, + FuncCall::class, + StaticCall::class, + New_::class, + ArrowFunction::class, + Closure::class, + ]; } /** - * @param ArrowFunction|Closure $node + * @param MethodCall|FuncCall|StaticCall|New_|ArrowFunction|Closure $node */ public function refactor(Node $node): null|CallLike { + if ($node instanceof CallLike) { + if ($node->isFirstClassCallable()) { + return null; + } + + $args = $node->getArgs(); + foreach ($args as $key => $arg) { + if ($arg->value instanceof Closure || $arg->value instanceof ArrowFunction) { + // verify caller signature + $methodReflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($node); + + if ($methodReflection === null) { + return null; + } + + $reflection = ParametersAcceptorSelectorVariantsWrapper::select( + $methodReflection, + $node, + ScopeFetcher::fetch($node) + ); + + if ($reflection instanceof ResolvedFunctionVariantWithOriginal) { + return null; + } + + foreach ($reflection->getParameters() as $index => $parameterReflection) { + if ($index === $key + && $parameterReflection->getType() + ->isCallable() + ->yes() + && count($parameterReflection->getType()->getParameters()) > 1 + ) { + $args[$key]->value->setAttribute(self::SKIP_BASED_ON_CALLBACK_SIGNATURE, true); + return null; + } + } + } + } + + return null; + } + $callLike = $this->extractCallLike($node); if ($callLike === null) { @@ -122,6 +181,10 @@ private function shouldSkip( return true; } + if ($node->getAttribute(self::SKIP_BASED_ON_CALLBACK_SIGNATURE) === true) { + return true; + } + $functionLike = $this->astResolver->resolveClassMethodOrFunctionFromCall($callLike); if (! $functionLike instanceof FunctionLike) { return false; From 1f734640b6eaf6a8a08dd472c1aa2e4c12aa366f Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 22 Oct 2025 21:50:56 +0700 Subject: [PATCH 2/4] const --- .../FunctionLike/FunctionLikeToFirstClassCallableRector.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index 99b480ca167..d5cc51fe2e3 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -40,7 +40,7 @@ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implem /** * @var string */ - private const SKIP_BASED_ON_CALLBACK_SIGNATURE = 'skip_based_on_callback_signature'; + private const HAS_CALLBACK_SIGNATURE_MULTI_PARAMS = 'has_callback_signature_multi_params'; public function __construct( private readonly AstResolver $astResolver, @@ -116,7 +116,7 @@ public function refactor(Node $node): null|CallLike ->yes() && count($parameterReflection->getType()->getParameters()) > 1 ) { - $args[$key]->value->setAttribute(self::SKIP_BASED_ON_CALLBACK_SIGNATURE, true); + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); return null; } } @@ -181,7 +181,7 @@ private function shouldSkip( return true; } - if ($node->getAttribute(self::SKIP_BASED_ON_CALLBACK_SIGNATURE) === true) { + if ($node->getAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS) === true) { return true; } From 94822001397f302d4f1f53111d4f7ef86d7f7185 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 22 Oct 2025 21:53:54 +0700 Subject: [PATCH 3/4] fix phpstan --- .../FunctionLike/FunctionLikeToFirstClassCallableRector.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index d5cc51fe2e3..170fb894537 100644 --- a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php +++ b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php @@ -111,9 +111,7 @@ public function refactor(Node $node): null|CallLike foreach ($reflection->getParameters() as $index => $parameterReflection) { if ($index === $key - && $parameterReflection->getType() - ->isCallable() - ->yes() + && $parameterReflection->getType() instanceof \PHPStan\Type\CallableType && count($parameterReflection->getType()->getParameters()) > 1 ) { $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); From 087c604536e1fb20aacaffd645fa87b33beb7677 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 22 Oct 2025 14:56:38 +0000 Subject: [PATCH 4/4] [ci-review] Rector Rectify --- .../FunctionLike/FunctionLikeToFirstClassCallableRector.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php b/rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php index 170fb894537..ff7c39e6ada 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 PHPStan\Type\CallableType; use PhpParser\Node; use PhpParser\Node\Arg; use PhpParser\Node\Expr; @@ -111,7 +112,7 @@ public function refactor(Node $node): null|CallLike foreach ($reflection->getParameters() as $index => $parameterReflection) { if ($index === $key - && $parameterReflection->getType() instanceof \PHPStan\Type\CallableType + && $parameterReflection->getType() instanceof CallableType && count($parameterReflection->getType()->getParameters()) > 1 ) { $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true);