From 10a1ad389536287266154e0b1d1b0b37be24aba6 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Fri, 13 Nov 2026 22:40:06 +0100 Subject: [PATCH] [CodingStyle] Skip closure with rebound $this via ->call()/->bindTo() on ArrowFunction/ClosureDelegatingCallToFirstClassCallableRector Fixes https://github.com/rectorphp/rector/issues/9964 --- .../Fixture/skip_issue_9964.php.inc | 27 ++++++++++++ .../Fixture/skip_rebound_closure.php.inc | 19 ++++++++ .../Fixture/skip_rebound_closure.php.inc | 27 ++++++++++++ ...ctionAndClosureFirstClassCallableGuard.php | 5 +++ .../LazyContainerFactory.php | 2 + src/NodeTypeResolver/Node/AttributeKey.php | 5 +++ .../NodeVisitor/ReboundClosureNodeVisitor.php | 44 +++++++++++++++++++ 7 files changed, 129 insertions(+) create mode 100644 rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_issue_9964.php.inc create mode 100644 rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc create mode 100644 rules-tests/CodingStyle/Rector/Closure/ClosureDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc create mode 100644 src/PhpParser/NodeVisitor/ReboundClosureNodeVisitor.php diff --git a/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_issue_9964.php.inc b/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_issue_9964.php.inc new file mode 100644 index 00000000000..72b4a106a3f --- /dev/null +++ b/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_issue_9964.php.inc @@ -0,0 +1,27 @@ +definitions[$id] = $definition; + } +} + +final class ContainerTest9964 +{ + public function test(): void + { + $container = new Container9964(); + + (fn (string $id, string $definition) => $this->addDefinition($id, $definition))->call( + $container, + 'test', + 'x', + ); + } +} diff --git a/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc b/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc new file mode 100644 index 00000000000..62808592947 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/ArrowFunction/ArrowFunctionDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc @@ -0,0 +1,19 @@ + $this->addDefinition($id, $definition))->call($container, 'test', 'x'); + + $bound = (fn (string $id, string $definition) => $this->addDefinition($id, $definition))->bindTo($container, $container); + + $bound = \Closure::bind(fn (string $id, string $definition) => $this->addDefinition($id, $definition), $container, $container); + } + + private function addDefinition(string $id, string $definition): void + { + } +} diff --git a/rules-tests/CodingStyle/Rector/Closure/ClosureDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc b/rules-tests/CodingStyle/Rector/Closure/ClosureDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc new file mode 100644 index 00000000000..026f1c69be9 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/Closure/ClosureDelegatingCallToFirstClassCallableRector/Fixture/skip_rebound_closure.php.inc @@ -0,0 +1,27 @@ +addDefinition($id); + })->call($container, 'test'); + + $bound = (function (string $id) { + return $this->addDefinition($id); + })->bindTo($container, $container); + + $bound = Closure::bind(function (string $id) { + return $this->addDefinition($id); + }, $container, $container); + } + + private function addDefinition(string $id): void + { + } +} diff --git a/rules/CodingStyle/Guard/ArrowFunctionAndClosureFirstClassCallableGuard.php b/rules/CodingStyle/Guard/ArrowFunctionAndClosureFirstClassCallableGuard.php index 6c58fc97f4a..508512e9896 100644 --- a/rules/CodingStyle/Guard/ArrowFunctionAndClosureFirstClassCallableGuard.php +++ b/rules/CodingStyle/Guard/ArrowFunctionAndClosureFirstClassCallableGuard.php @@ -50,6 +50,11 @@ public function shouldSkip( return true; } + // $this is rebound, e.g. (fn () => $this->method())->call($object) + if ($arrowFunctionOrClosure->getAttribute(AttributeKey::IS_REBOUND_CLOSURE) === true) { + return true; + } + if ($arrowFunctionOrClosure->getAttribute( AttributeKey::IS_ASSIGNED_TO ) === true || $arrowFunctionOrClosure->getAttribute(AttributeKey::IS_BEING_ASSIGNED)) { diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 8b37e0bfd87..e067b92e2db 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -52,6 +52,7 @@ use Rector\PhpParser\NodeVisitor\LocalVariableScopeNodeVisitor; use Rector\PhpParser\NodeVisitor\NameAndArgNodeVisitor; use Rector\PhpParser\NodeVisitor\PhpVersionConditionNodeVisitor; +use Rector\PhpParser\NodeVisitor\ReboundClosureNodeVisitor; use Rector\PostRector\Application\PostFileProcessor; use Rector\Rector\AbstractRector; use Symfony\Component\Console\Application; @@ -72,6 +73,7 @@ final class LazyContainerFactory LocalVariableScopeNodeVisitor::class, NameAndArgNodeVisitor::class, DefaultValueNodeVisitor::class, + ReboundClosureNodeVisitor::class, ]; /** diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 0539d4b4dce..7e3aa62b7e3 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -169,4 +169,9 @@ final class AttributeKey * 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'; + + /** + * Closure or arrow function with rebound $this, e.g. (fn () => ...)->call($object) + */ + public const string IS_REBOUND_CLOSURE = 'is_rebound_closure'; } diff --git a/src/PhpParser/NodeVisitor/ReboundClosureNodeVisitor.php b/src/PhpParser/NodeVisitor/ReboundClosureNodeVisitor.php new file mode 100644 index 00000000000..241b7fd2ba6 --- /dev/null +++ b/src/PhpParser/NodeVisitor/ReboundClosureNodeVisitor.php @@ -0,0 +1,44 @@ + ...)->call($object) + * @see https://github.com/rectorphp/rector/issues/9964 + */ +final class ReboundClosureNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface +{ + public function enterNode(Node $node): ?Node + { + if (! $node instanceof MethodCall) { + return null; + } + + if (! $node->var instanceof Closure && ! $node->var instanceof ArrowFunction) { + return null; + } + + if (! $node->name instanceof Identifier) { + return null; + } + + if (! in_array($node->name->toLowerString(), ['call', 'bindto'], true)) { + return null; + } + + $node->var->setAttribute(AttributeKey::IS_REBOUND_CLOSURE, true); + + return null; + } +}