Skip to content
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Fixture;

use Closure;
use Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject;

class FixtureWithNamedThisBinding
{
public function first()
{
return BindObject::call(closure: function () {
return 'bar';
}, closureTwo: function () {
return 'foo';
});
}
}

?>
-----
<?php

namespace Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Fixture;

use Closure;
use Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject;

class FixtureWithNamedThisBinding
{
public function first()
{
return BindObject::call(closure: function () {
return 'bar';
}, closureTwo: static function () {
return 'foo';
});
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

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';
});

(new BindObject())->callOnObject(function () {
echo 'method call';
});

bind_on_object(function () {
echo 'closure func call 0';
}, null, function () {
echo 'closure func call 0';
});

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source;

class BindObject
{
/**
* @param-closure-this \Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject $closure
*/
public static function call(\Closure $closure, \Closure|null $closureTwo = null)
{

}

/**
* @param-closure-this \Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject $closure
*/
public function callOnObject(\Closure $closure)
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source;

use Closure;

/**
* @param-closure-this \Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject $closure
* @param-closure-this \Rector\Tests\CodingStyle\Rector\Closure\StaticClosureRector\Source\BindObject $closureFinally
*/
function bind_on_object(Closure|null $closure = null, Closure|null $anotherClosure = null, Closure|null $closureFinally = null)
{

}
7 changes: 6 additions & 1 deletion rules/CodingStyle/Rector/Closure/StaticClosureRector.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use Rector\CodingStyle\Guard\StaticGuard;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
Expand All @@ -17,7 +18,7 @@
final class StaticClosureRector extends AbstractRector
{
public function __construct(
private readonly StaticGuard $staticGuard
private readonly StaticGuard $staticGuard,
) {
}

Expand Down Expand Up @@ -64,6 +65,10 @@ public function getNodeTypes(): array
*/
public function refactor(Node $node): ?Node
{
if ($node->hasAttribute(AttributeKey::IS_CLOSURE_USES_THIS)) {
return null;
}

if (! $this->staticGuard->isLegal($node)) {
return null;
}
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/LazyContainerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -256,6 +257,7 @@ final class LazyContainerFactory
PropertyOrClassConstDefaultNodeVisitor::class,
ParamDefaultNodeVisitor::class,
ClassConstFetchNodeVisitor::class,
CallLikeThisBoundClosureArgsNodeVisitor::class,
];

/**
Expand Down
91 changes: 91 additions & 0 deletions src/NodeAnalyzer/CallLikeExpectsThisBoundClosureArgsAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Rector\NodeAnalyzer;

use PhpParser\Node\Arg;
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\Reflection\ReflectionResolver;

final readonly class CallLikeExpectsThisBoundClosureArgsAnalyzer
{
public function __construct(
private ReflectionResolver $reflectionResolver
) {
}

/**
* @return Arg[]
*/
public function getArgsUsingThisBoundClosure(CallLike $callLike): array
{
if ($callLike->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;
}
}
2 changes: 2 additions & 0 deletions src/NodeTypeResolver/Node/AttributeKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace Rector\PhpParser\NodeVisitor;

use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\NodeVisitorAbstract;
use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface;
use Rector\NodeAnalyzer\CallLikeExpectsThisBoundClosureArgsAnalyzer;
use Rector\NodeTypeResolver\Node\AttributeKey;

final class CallLikeThisBoundClosureArgsNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface
{
public function __construct(
private readonly CallLikeExpectsThisBoundClosureArgsAnalyzer $callLikeExpectsThisBindedClosureArgsAnalyzer
) {
}

public function enterNode(Node $node): ?Node
{
if (
! $node instanceof MethodCall
&& ! $node instanceof StaticCall
&& ! $node instanceof FuncCall
) {
return null;
}

if ($node->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;
}
}