From 3cefafa13a58f51d4d9e1937531af644bd00f1c6 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Fri, 5 Dec 2025 20:03:44 +0000 Subject: [PATCH 01/11] working --- composer.json | 3 +- .../fixture_with_named_this_binding.php.inc | 41 +++++++++++ .../skip_use_with_context_binding.php.inc | 21 ++++++ .../StaticClosureRector/Source/BindObject.php | 22 ++++++ .../StaticClosureRector/Source/functions.php | 10 +++ .../Rector/Closure/StaticClosureRector.php | 47 ++++++++++++- ...keExpectsThisBindedClosureArgsAnalyzer.php | 68 +++++++++++++++++++ 7 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/fixture_with_named_this_binding.php.inc create mode 100644 rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc create mode 100644 rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/BindObject.php create mode 100644 rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php create mode 100644 src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php diff --git a/composer.json b/composer.json index 311704292d2..eb0cf0cfcbe 100644 --- a/composer.json +++ b/composer.json @@ -98,7 +98,8 @@ "tests/debug_functions.php", "rules-tests/Transform/Rector/FuncCall/FuncCallToMethodCallRector/Source/some_view_function.php", "rules-tests/TypeDeclaration/Rector/ClassMethod/ParamTypeByMethodCallTypeRector/Source/FunctionTyped.php", - "rules-tests/Php70/Rector/ClassMethod/Php4ConstructorRector/Source/ParentClass.php" + "rules-tests/Php70/Rector/ClassMethod/Php4ConstructorRector/Source/ParentClass.php", + "rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php" ] }, "scripts": { diff --git a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/fixture_with_named_this_binding.php.inc b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/fixture_with_named_this_binding.php.inc new file mode 100644 index 00000000000..3d35c44b0f3 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/fixture_with_named_this_binding.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc new file mode 100644 index 00000000000..052dc3efbc3 --- /dev/null +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc @@ -0,0 +1,21 @@ +callOnObject(function () { + echo 'method call'; +}); + +bind_on_object(function () { + echo 'closure func call 0'; +}, null, function () { + echo 'closure func call 0'; +}); + +?> diff --git a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/BindObject.php b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/BindObject.php new file mode 100644 index 00000000000..cd4881bf0ed --- /dev/null +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/BindObject.php @@ -0,0 +1,22 @@ +traverseNodesWithCallable($nodes, function (Node $node): ?CallLike { + if ( + ! $node instanceof Node\Expr\MethodCall + && ! $node instanceof Node\Expr\StaticCall + && ! $node instanceof Node\Expr\FuncCall + ) { + return null; + } + + if ($node->isFirstClassCallable()) { + return null; + } + + $args = $this->callLikeExpectsThisBindedClosureArgsAnalyzer->getArgsUsingThisBindedClosure($node); + + if ($args === []) { + return null; + } + + foreach ($args as $arg) { + if ($arg->value instanceof Closure && ! $arg->hasAttribute(self::CLOSURE_USES_THIS)) { + $arg->value->setAttribute(self::CLOSURE_USES_THIS, true); + } + } + + return $node; + }); + + return $nodes; + } + /** * @param Closure $node */ public function refactor(Node $node): ?Node { + if ($node->hasAttribute(self::CLOSURE_USES_THIS)) { + return null; + } + if (! $this->staticGuard->isLegal($node)) { return null; } diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php new file mode 100644 index 00000000000..e683e8033ee --- /dev/null +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -0,0 +1,68 @@ +reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); + + if ($reflection === null) { + return []; + } + + $scope = ScopeFetcher::fetch($callLike); + + $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); + $parameters = $parametersAcceptor->getParameters(); + + foreach ($callLike->getArgs() as $index => $arg) { + + if (! $arg->value instanceof Closure) { + continue; + } + + if ($arg->name?->name !== null) { + /** @var ParameterReflectionWithPhpDocs $parameter */ + foreach ($parameters as $parameter) { + $hasObjectBinding = (bool) $parameter->getClosureThisType(); + if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { + $args[] = $arg; + } + } + + continue; + } + + if ($arg->name?->name === null) { + /** @var ParameterReflectionWithPhpDocs $parameter */ + $parameter = $parameters[$index] ?? null; + + if ($parameter === null) { + continue; + } + + if ($parameter->getClosureThisType() !== null) { + $args[] = $arg; + } + } + } + + return $args; + } +} From ae4853f5f3c703985ca1ca105cc4c6e1e5835a73 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Fri, 5 Dec 2025 20:23:09 +0000 Subject: [PATCH 02/11] fixes --- .../Rector/Closure/StaticClosureRector.php | 10 +++++--- ...keExpectsThisBindedClosureArgsAnalyzer.php | 23 +++++++++++++------ 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php index 40dbb047373..1ec95bececc 100644 --- a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php +++ b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php @@ -4,9 +4,13 @@ namespace Rector\CodingStyle\Rector\Closure; +use Override; use PhpParser\Node; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; +use PhpParser\Node\Expr\FuncCall; +use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\StaticCall; use Rector\CodingStyle\Guard\StaticGuard; use Rector\NodeAnalyzer\CallLikeExpectsThisBindedClosureArgsAnalyzer; use Rector\Rector\AbstractRector; @@ -71,9 +75,9 @@ public function beforeTraverse(array $nodes): array $this->traverseNodesWithCallable($nodes, function (Node $node): ?CallLike { if ( - ! $node instanceof Node\Expr\MethodCall - && ! $node instanceof Node\Expr\StaticCall - && ! $node instanceof Node\Expr\FuncCall + ! $node instanceof MethodCall + && ! $node instanceof StaticCall + && ! $node instanceof FuncCall ) { return null; } diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index e683e8033ee..56932a46135 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -1,27 +1,38 @@ reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); + if ($callLike->isFirstClassCallable()) { + return []; + } + if ($reflection === null) { return []; } @@ -38,7 +49,6 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array } if ($arg->name?->name !== null) { - /** @var ParameterReflectionWithPhpDocs $parameter */ foreach ($parameters as $parameter) { $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { @@ -49,8 +59,7 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array continue; } - if ($arg->name?->name === null) { - /** @var ParameterReflectionWithPhpDocs $parameter */ + if (! is_string($arg->name?->name)) { $parameter = $parameters[$index] ?? null; if ($parameter === null) { From 6394422d6efd8caf4c97bed79d2c7bae1c48b45e Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Fri, 5 Dec 2025 23:16:51 +0000 Subject: [PATCH 03/11] fixes --- rules/CodingStyle/Rector/Closure/StaticClosureRector.php | 2 -- .../CallLikeExpectsThisBindedClosureArgsAnalyzer.php | 8 +++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php index 1ec95bececc..8e92019407a 100644 --- a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php +++ b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php @@ -4,7 +4,6 @@ namespace Rector\CodingStyle\Rector\Closure; -use Override; use PhpParser\Node; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; @@ -68,7 +67,6 @@ public function getNodeTypes(): array return [Closure::class]; } - #[Override] public function beforeTraverse(array $nodes): array { parent::beforeTraverse($nodes); diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index 56932a46135..683402d009c 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -12,7 +12,7 @@ use Rector\PHPStan\ScopeFetcher; use Rector\Reflection\ReflectionResolver; -class CallLikeExpectsThisBindedClosureArgsAnalyzer +final class CallLikeExpectsThisBindedClosureArgsAnalyzer { public function __construct( private readonly ReflectionResolver $reflectionResolver @@ -43,13 +43,13 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array $parameters = $parametersAcceptor->getParameters(); foreach ($callLike->getArgs() as $index => $arg) { - if (! $arg->value instanceof Closure) { continue; } if ($arg->name?->name !== null) { foreach ($parameters as $parameter) { + /** @phpstan-ignore method.notFound */ $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { $args[] = $arg; @@ -66,7 +66,9 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array continue; } - if ($parameter->getClosureThisType() !== null) { + /** @phpstan-ignore method.notFound */ + $hasObjectBinding = (bool) $parameter->getClosureThisType(); + if ($hasObjectBinding) { $args[] = $arg; } } From f8d993f904d0677ee40e7290c2d8aed6b3a844bf Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sat, 6 Dec 2025 02:19:15 +0000 Subject: [PATCH 04/11] Fix PHPStan issues --- ...lLikeExpectsThisBindedClosureArgsAnalyzer.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index 683402d009c..6270dbee46c 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -7,22 +7,20 @@ use PhpParser\Node\Arg; use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; -use PHPStan\Reflection\ParameterReflectionWithPhpDocs; +use PHPStan\Reflection\ExtendedParameterReflection; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PHPStan\ScopeFetcher; use Rector\Reflection\ReflectionResolver; -final class CallLikeExpectsThisBindedClosureArgsAnalyzer +final readonly class CallLikeExpectsThisBindedClosureArgsAnalyzer { public function __construct( - private readonly ReflectionResolver $reflectionResolver + private ReflectionResolver $reflectionResolver ) { } /** - * @param CallLike $callLike * @return Arg[] - * @throws \Rector\Exception\ShouldNotHappenException */ public function getArgsUsingThisBindedClosure(CallLike $callLike): array { @@ -49,7 +47,10 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array if ($arg->name?->name !== null) { foreach ($parameters as $parameter) { - /** @phpstan-ignore method.notFound */ + if (! $parameter instanceof ExtendedParameterReflection) { + continue; + } + $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { $args[] = $arg; @@ -62,11 +63,10 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array if (! is_string($arg->name?->name)) { $parameter = $parameters[$index] ?? null; - if ($parameter === null) { + if (! $parameter instanceof ExtendedParameterReflection) { continue; } - /** @phpstan-ignore method.notFound */ $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding) { $args[] = $arg; From a1789e8e91ad8ac64a570b2522c0ea5d5656b0e7 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 7 Dec 2025 14:58:39 +0000 Subject: [PATCH 05/11] wip --- .../Rector/Closure/StaticClosureRector.php | 47 ++--------------- .../LazyContainerFactory.php | 2 + ...keExpectsThisBindedClosureArgsAnalyzer.php | 3 +- src/NodeTypeResolver/Node/AttributeKey.php | 2 + ...allLikeThisBoundClosureArgsNodeVisitor.php | 52 +++++++++++++++++++ 5 files changed, 61 insertions(+), 45 deletions(-) create mode 100644 src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php diff --git a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php index 8e92019407a..45b8fad5dc4 100644 --- a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php +++ b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php @@ -5,13 +5,10 @@ namespace Rector\CodingStyle\Rector\Closure; use PhpParser\Node; -use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; -use PhpParser\Node\Expr\FuncCall; -use PhpParser\Node\Expr\MethodCall; -use PhpParser\Node\Expr\StaticCall; use Rector\CodingStyle\Guard\StaticGuard; -use Rector\NodeAnalyzer\CallLikeExpectsThisBindedClosureArgsAnalyzer; +use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\CallLikeThisBoundClosureArgsNodeVisitor; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -21,11 +18,8 @@ */ final class StaticClosureRector extends AbstractRector { - private const CLOSURE_USES_THIS = 'has_this_closure'; - public function __construct( private readonly StaticGuard $staticGuard, - private readonly CallLikeExpectsThisBindedClosureArgsAnalyzer $callLikeExpectsThisBindedClosureArgsAnalyzer ) { } @@ -67,47 +61,12 @@ public function getNodeTypes(): array return [Closure::class]; } - public function beforeTraverse(array $nodes): array - { - parent::beforeTraverse($nodes); - - $this->traverseNodesWithCallable($nodes, function (Node $node): ?CallLike { - if ( - ! $node instanceof MethodCall - && ! $node instanceof StaticCall - && ! $node instanceof FuncCall - ) { - return null; - } - - if ($node->isFirstClassCallable()) { - return null; - } - - $args = $this->callLikeExpectsThisBindedClosureArgsAnalyzer->getArgsUsingThisBindedClosure($node); - - if ($args === []) { - return null; - } - - foreach ($args as $arg) { - if ($arg->value instanceof Closure && ! $arg->hasAttribute(self::CLOSURE_USES_THIS)) { - $arg->value->setAttribute(self::CLOSURE_USES_THIS, true); - } - } - - return $node; - }); - - return $nodes; - } - /** * @param Closure $node */ public function refactor(Node $node): ?Node { - if ($node->hasAttribute(self::CLOSURE_USES_THIS)) { + if ($node->hasAttribute(AttributeKey::CLOSURE_USES_THIS)) { return null; } diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 5065871eae1..670ef55e808 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -100,6 +100,7 @@ use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\AssignedToNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ByRefReturnNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ByRefVariableNodeVisitor; +use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\CallLikeThisBoundClosureArgsNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ClassConstFetchNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ContextNodeVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\GlobalVariableNodeVisitor; @@ -252,6 +253,7 @@ final class LazyContainerFactory StaticVariableNodeVisitor::class, PropertyOrClassConstDefaultNodeVisitor::class, ClassConstFetchNodeVisitor::class, + CallLikeThisBoundClosureArgsNodeVisitor::class, ]; /** diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index 6270dbee46c..fc87ce257c5 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -8,6 +8,7 @@ use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; use PHPStan\Reflection\ExtendedParameterReflection; +use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PHPStan\ScopeFetcher; use Rector\Reflection\ReflectionResolver; @@ -35,7 +36,7 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array return []; } - $scope = ScopeFetcher::fetch($callLike); + $scope = $callLike->getAttribute(AttributeKey::SCOPE); $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); $parameters = $parametersAcceptor->getParameters(); diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 10ca311bff9..35f2459dc96 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -288,4 +288,6 @@ final class AttributeKey public const CLASS_CONST_FETCH_NAME = 'class_const_fetch_name'; public const PHP_VERSION_CONDITIONED = 'php_version_conditioned'; + + public const CLOSURE_USES_THIS = 'has_this_closure'; } diff --git a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php new file mode 100644 index 00000000000..f1f5f5bedc9 --- /dev/null +++ b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php @@ -0,0 +1,52 @@ +isFirstClassCallable()) { + return null; + } + + $args = $this->callLikeExpectsThisBindedClosureArgsAnalyzer->getArgsUsingThisBindedClosure($node); + + if ($args === []) { + return null; + } + + foreach ($args as $arg) { + if ($arg->value instanceof Closure && ! $arg->hasAttribute(AttributeKey::CLOSURE_USES_THIS)) { + $arg->value->setAttribute(AttributeKey::CLOSURE_USES_THIS, true); + } + } + + return $node; + } +} From 91e3be05acbde9f18e2719e11764f374b60d58b0 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 7 Dec 2025 15:31:51 +0000 Subject: [PATCH 06/11] fixes --- .../CodingStyle/Rector/Closure/StaticClosureRector.php | 3 +-- .../CallLikeExpectsThisBindedClosureArgsAnalyzer.php | 4 +--- src/NodeTypeResolver/Node/AttributeKey.php | 2 +- .../CallLikeThisBoundClosureArgsNodeVisitor.php | 10 +++++----- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php index 45b8fad5dc4..8d50fe1fec2 100644 --- a/rules/CodingStyle/Rector/Closure/StaticClosureRector.php +++ b/rules/CodingStyle/Rector/Closure/StaticClosureRector.php @@ -8,7 +8,6 @@ use PhpParser\Node\Expr\Closure; use Rector\CodingStyle\Guard\StaticGuard; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\CallLikeThisBoundClosureArgsNodeVisitor; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -66,7 +65,7 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - if ($node->hasAttribute(AttributeKey::CLOSURE_USES_THIS)) { + if ($node->hasAttribute(AttributeKey::IS_CLOSURE_USES_THIS)) { return null; } diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index fc87ce257c5..3844cb4bad3 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -8,7 +8,6 @@ use PhpParser\Node\Expr\CallLike; use PhpParser\Node\Expr\Closure; use PHPStan\Reflection\ExtendedParameterReflection; -use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; use Rector\PHPStan\ScopeFetcher; use Rector\Reflection\ReflectionResolver; @@ -36,8 +35,7 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array return []; } - $scope = $callLike->getAttribute(AttributeKey::SCOPE); - + $scope = ScopeFetcher::fetch($callLike); $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); $parameters = $parametersAcceptor->getParameters(); diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index 35f2459dc96..dba9c84687b 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -289,5 +289,5 @@ final class AttributeKey public const PHP_VERSION_CONDITIONED = 'php_version_conditioned'; - public const CLOSURE_USES_THIS = 'has_this_closure'; + public const IS_CLOSURE_USES_THIS = 'has_this_closure'; } diff --git a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php index f1f5f5bedc9..e203fcc01fb 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php +++ b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php @@ -1,5 +1,7 @@ value instanceof Closure && ! $arg->hasAttribute(AttributeKey::CLOSURE_USES_THIS)) { - $arg->value->setAttribute(AttributeKey::CLOSURE_USES_THIS, true); + if ($arg->value instanceof Closure && ! $arg->hasAttribute(AttributeKey::IS_CLOSURE_USES_THIS)) { + $arg->value->setAttribute(AttributeKey::IS_CLOSURE_USES_THIS, true); } } From b11216c1d308718dd91f6d03d16ee6a93a8fd6b0 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 7 Dec 2025 15:34:41 +0000 Subject: [PATCH 07/11] final class --- .../NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php index e203fcc01fb..4b56b163687 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php +++ b/src/NodeTypeResolver/PHPStan/Scope/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php @@ -14,7 +14,7 @@ use Rector\NodeAnalyzer\CallLikeExpectsThisBindedClosureArgsAnalyzer; use Rector\NodeTypeResolver\Node\AttributeKey; -class CallLikeThisBoundClosureArgsNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface +final class CallLikeThisBoundClosureArgsNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface { public function __construct( private readonly CallLikeExpectsThisBindedClosureArgsAnalyzer $callLikeExpectsThisBindedClosureArgsAnalyzer From 6b6ae0023bc4dd8c40d4b06b522dd98f7853d135 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Sun, 7 Dec 2025 15:51:45 +0000 Subject: [PATCH 08/11] fixes --- .../Fixture/skip_use_with_context_binding.php.inc | 1 + .../Closure/StaticClosureRector/Source/functions.php | 4 ++++ .../CallLikeExpectsThisBindedClosureArgsAnalyzer.php | 9 +++++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc index 052dc3efbc3..9a5c1ef797a 100644 --- a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc @@ -3,6 +3,7 @@ namespace Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Fixture; use Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject; +use function Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\bind_on_object; BindObject::call(closure: function () { echo 'static call'; diff --git a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php index ef6fd506990..37cef34c0d4 100644 --- a/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Source/functions.php @@ -1,5 +1,9 @@ getAttribute(AttributeKey::SCOPE); + + if ($scope === null) { + return []; + } + $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); $parameters = $parametersAcceptor->getParameters(); From 7a05a93189e950d161f70091aeab8c5d6a5d054d Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Tue, 9 Dec 2025 15:35:57 +0000 Subject: [PATCH 09/11] Additional param check --- .../CallLikeExpectsThisBindedClosureArgsAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php index f53ada4d004..74ca51b9535 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBindedClosureArgsAnalyzer.php @@ -27,7 +27,7 @@ public function getArgsUsingThisBindedClosure(CallLike $callLike): array $args = []; $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); - if ($callLike->isFirstClassCallable()) { + if ($callLike->isFirstClassCallable() || $callLike->getArgs() === []) { return []; } From bec4f461de5659d964f9cac9fef9dabb9cd2b093 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Wed, 24 Dec 2025 02:02:28 +0000 Subject: [PATCH 10/11] feedback changes --- .../CallLikeExpectsThisBoundClosureArgsAnalyzer.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php index e5e64b711fd..8cfa76a335e 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php @@ -25,12 +25,20 @@ public function __construct( public function getArgsUsingThisBoundClosure(CallLike $callLike): array { $args = []; - $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); if ($callLike->isFirstClassCallable() || $callLike->getArgs() === []) { return []; } + $args = $callLike->getArgs(); + $hasClosureArg = (bool) array_filter($args, fn (Arg $arg): bool => $arg->value instanceof Closure); + + if (! $hasClosureArg) { + return []; + } + + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); + if ($reflection === null) { return []; } From 7ccd97cfabe8b8dd5615eaf42cb69da1c3c6e8a9 Mon Sep 17 00:00:00 2001 From: Peter Fox Date: Wed, 24 Dec 2025 02:08:48 +0000 Subject: [PATCH 11/11] fix --- ...llLikeExpectsThisBoundClosureArgsAnalyzer.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php index 8cfa76a335e..a5039df661c 100644 --- a/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php +++ b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php @@ -24,19 +24,19 @@ public function __construct( */ public function getArgsUsingThisBoundClosure(CallLike $callLike): array { - $args = []; - if ($callLike->isFirstClassCallable() || $callLike->getArgs() === []) { return []; } - $args = $callLike->getArgs(); - $hasClosureArg = (bool) array_filter($args, fn (Arg $arg): bool => $arg->value instanceof Closure); + $callArgs = $callLike->getArgs(); + $hasClosureArg = (bool) array_filter($callArgs, fn (Arg $arg): bool => $arg->value instanceof Closure); if (! $hasClosureArg) { return []; } + $argsUsingThisBoundClosure = []; + $reflection = $this->reflectionResolver->resolveFunctionLikeReflectionFromCall($callLike); if ($reflection === null) { @@ -52,7 +52,7 @@ public function getArgsUsingThisBoundClosure(CallLike $callLike): array $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); $parameters = $parametersAcceptor->getParameters(); - foreach ($callLike->getArgs() as $index => $arg) { + foreach ($callArgs as $index => $arg) { if (! $arg->value instanceof Closure) { continue; } @@ -65,7 +65,7 @@ public function getArgsUsingThisBoundClosure(CallLike $callLike): array $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { - $args[] = $arg; + $argsUsingThisBoundClosure[] = $arg; } } @@ -81,11 +81,11 @@ public function getArgsUsingThisBoundClosure(CallLike $callLike): array $hasObjectBinding = (bool) $parameter->getClosureThisType(); if ($hasObjectBinding) { - $args[] = $arg; + $argsUsingThisBoundClosure[] = $arg; } } } - return $args; + return $argsUsingThisBoundClosure; } }