diff --git a/composer.json b/composer.json index ce878e83d21..b8f6b6e1b63 100644 --- a/composer.json +++ b/composer.json @@ -99,7 +99,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..9a5c1ef797a --- /dev/null +++ b/rules-tests/CodingStyle/Rector/Closure/StaticClosureRector/Fixture/skip_use_with_context_binding.php.inc @@ -0,0 +1,22 @@ +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 @@ +hasAttribute(AttributeKey::IS_CLOSURE_USES_THIS)) { + return null; + } + if (! $this->staticGuard->isLegal($node)) { return null; } diff --git a/src/DependencyInjection/LazyContainerFactory.php b/src/DependencyInjection/LazyContainerFactory.php index 9379d8cb6b2..f0c7fb6c643 100644 --- a/src/DependencyInjection/LazyContainerFactory.php +++ b/src/DependencyInjection/LazyContainerFactory.php @@ -120,6 +120,7 @@ use Rector\PhpParser\NodeVisitor\AssignedToNodeVisitor; use Rector\PhpParser\NodeVisitor\ByRefReturnNodeVisitor; use Rector\PhpParser\NodeVisitor\ByRefVariableNodeVisitor; +use Rector\PhpParser\NodeVisitor\CallLikeThisBoundClosureArgsNodeVisitor; use Rector\PhpParser\NodeVisitor\ClassConstFetchNodeVisitor; use Rector\PhpParser\NodeVisitor\ClosureWithVariadicParametersNodeVisitor; use Rector\PhpParser\NodeVisitor\ContextNodeVisitor; @@ -256,6 +257,7 @@ final class LazyContainerFactory PropertyOrClassConstDefaultNodeVisitor::class, ParamDefaultNodeVisitor::class, ClassConstFetchNodeVisitor::class, + CallLikeThisBoundClosureArgsNodeVisitor::class, ]; /** diff --git a/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php new file mode 100644 index 00000000000..a5039df661c --- /dev/null +++ b/src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php @@ -0,0 +1,91 @@ +isFirstClassCallable() || $callLike->getArgs() === []) { + return []; + } + + $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) { + return []; + } + + $scope = $callLike->getAttribute(AttributeKey::SCOPE); + + if ($scope === null) { + return []; + } + + $parametersAcceptor = ParametersAcceptorSelectorVariantsWrapper::select($reflection, $callLike, $scope); + $parameters = $parametersAcceptor->getParameters(); + + foreach ($callArgs as $index => $arg) { + if (! $arg->value instanceof Closure) { + continue; + } + + if ($arg->name?->name !== null) { + foreach ($parameters as $parameter) { + if (! $parameter instanceof ExtendedParameterReflection) { + continue; + } + + $hasObjectBinding = (bool) $parameter->getClosureThisType(); + if ($hasObjectBinding && $arg->name->name === $parameter->getName()) { + $argsUsingThisBoundClosure[] = $arg; + } + } + + continue; + } + + if (! is_string($arg->name?->name)) { + $parameter = $parameters[$index] ?? null; + + if (! $parameter instanceof ExtendedParameterReflection) { + continue; + } + + $hasObjectBinding = (bool) $parameter->getClosureThisType(); + if ($hasObjectBinding) { + $argsUsingThisBoundClosure[] = $arg; + } + } + } + + return $argsUsingThisBoundClosure; + } +} diff --git a/src/NodeTypeResolver/Node/AttributeKey.php b/src/NodeTypeResolver/Node/AttributeKey.php index fc29bc182fa..46a6d5a0037 100644 --- a/src/NodeTypeResolver/Node/AttributeKey.php +++ b/src/NodeTypeResolver/Node/AttributeKey.php @@ -274,6 +274,8 @@ final class AttributeKey public const PHP_VERSION_CONDITIONED = 'php_version_conditioned'; + public const IS_CLOSURE_USES_THIS = 'has_this_closure'; + public const HAS_CLOSURE_WITH_VARIADIC_ARGS = 'has_closure_with_variadic_args'; public const IS_IN_TRY_BLOCK = 'is_in_try_block'; diff --git a/src/PhpParser/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php b/src/PhpParser/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php new file mode 100644 index 00000000000..96a5ce9c64a --- /dev/null +++ b/src/PhpParser/NodeVisitor/CallLikeThisBoundClosureArgsNodeVisitor.php @@ -0,0 +1,52 @@ +isFirstClassCallable()) { + return null; + } + + $args = $this->callLikeExpectsThisBindedClosureArgsAnalyzer->getArgsUsingThisBoundClosure($node); + + if ($args === []) { + return null; + } + + foreach ($args as $arg) { + if ($arg->value instanceof Closure && ! $arg->hasAttribute(AttributeKey::IS_CLOSURE_USES_THIS)) { + $arg->value->setAttribute(AttributeKey::IS_CLOSURE_USES_THIS, true); + } + } + + return $node; + } +}