Skip to content

Commit

Permalink
[Downgrade PHP 7.0] Add DowngradeClosureCallRector (#1304)
Browse files Browse the repository at this point in the history
* [Downgrade PHP 7.0] Add DowngradeClosureCallRector

* Fix rule definition

* Fix typo in comment

* Split methods
  • Loading branch information
villfa authored Nov 25, 2021
1 parent aa72166 commit 50a78a6
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 0 deletions.
2 changes: 2 additions & 0 deletions config/set/downgrade-php70.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Rector\DowngradePhp70\Rector\FuncCall\DowngradeSessionStartArrayOptionsRector;
use Rector\DowngradePhp70\Rector\FunctionLike\DowngradeScalarTypeDeclarationRector;
use Rector\DowngradePhp70\Rector\GroupUse\SplitGroupedUseImportsRector;
use Rector\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector;
use Rector\DowngradePhp70\Rector\New_\DowngradeAnonymousClassRector;
use Rector\DowngradePhp70\Rector\Spaceship\DowngradeSpaceshipRector;
use Rector\DowngradePhp70\Rector\String_\DowngradeGeneratedScalarTypesRector;
Expand All @@ -33,6 +34,7 @@
$services->set(DowngradeDirnameLevelsRector::class);
$services->set(DowngradeSessionStartArrayOptionsRector::class);
$services->set(SplitGroupedUseImportsRector::class);
$services->set(DowngradeClosureCallRector::class);
$services->set(DowngradeGeneratedScalarTypesRector::class);
$services->set(DowngradeParentTypeDeclarationRector::class);
};
81 changes: 81 additions & 0 deletions packages/NodeTypeResolver/TypeAnalyzer/MethodTypeAnalyzer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Rector\NodeTypeResolver\TypeAnalyzer;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Identifier;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\TypeWithClassName;
use Rector\NodeTypeResolver\NodeTypeResolver;

final class MethodTypeAnalyzer
{
public function __construct(
private NodeTypeResolver $nodeTypeResolver
) {
}

/**
* @param class-string $expectedClass
* @param non-empty-string $expectedMethod
*/
public function isCallTo(MethodCall $methodCall, string $expectedClass, string $expectedMethod): bool
{
if (! $this->isMethodName($methodCall, $expectedMethod)) {
return false;
}

return $this->isInstanceOf($methodCall->var, $expectedClass);
}

/**
* @param non-empty-string $expectedName
*/
private function isMethodName(MethodCall $methodCall, string $expectedName): bool
{
if (
$methodCall->name instanceof Identifier
&& $this->areMethodNamesEqual($methodCall->name->toString(), $expectedName)
) {
return true;
}

$type = $this->nodeTypeResolver->getType($methodCall->name);

return $type instanceof ConstantStringType && $this->areMethodNamesEqual($type->getValue(), $expectedName);
}

private function areMethodNamesEqual(string $left, string $right): bool
{
$comparison = strcasecmp($left, $right);

return $comparison === 0;
}

/**
* @param class-string $expectedClass
*/
private function isInstanceOf(Expr $expr, string $expectedClass): bool
{
$type = $this->nodeTypeResolver->getType($expr);
if (! $type instanceof TypeWithClassName) {
return false;
}

if ($this->areClassNamesEqual($expectedClass, $type->getClassName())) {
return true;
}

return $type->getAncestorWithClassName($expectedClass) !== null;
}

private function areClassNamesEqual(string $left, string $right): bool
{
$comparison = strcasecmp($left, $right);

return $comparison === 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;

final class DowngradeClosureCallRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}

/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\Fixture;

class Foo
{
public $value = 'foo';
}

$closure = function () {
echo $this->value, PHP_EOL;
};
$args = [];

$closure->call(new Foo(), ...$args);

?>
-----
<?php

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\Fixture;

class Foo
{
public $value = 'foo';
}

$closure = function () {
echo $this->value, PHP_EOL;
};
$args = [];

call_user_func($closure->bindTo(...array_fill(0, 2, new Foo())), ...$args);

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

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\Fixture;

class Foo
{
public $value = 'foo';
}

$foo = new Foo();
$closure = function () {
echo $this->value, PHP_EOL;
};
$args = [];

$closure->call($foo, ...$args);

?>
-----
<?php

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\Fixture;

class Foo
{
public $value = 'foo';
}

$foo = new Foo();
$closure = function () {
echo $this->value, PHP_EOL;
};
$args = [];

call_user_func($closure->bindTo($foo, $foo), ...$args);

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

namespace Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\Fixture;

class Foo
{
public function call($obj)
{
var_dump($obj);
}
}

$foo = new Foo();
$foo->call($bar);

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

declare(strict_types=1);

use Rector\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(DowngradeClosureCallRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

declare(strict_types=1);

namespace Rector\DowngradePhp70\Rector\MethodCall;

use Closure;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\LNumber;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeTypeResolver\TypeAnalyzer\MethodTypeAnalyzer;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://wiki.php.net/rfc/closure_apply
*
* @see \Rector\Tests\DowngradePhp70\Rector\MethodCall\DowngradeClosureCallRector\DowngradeClosureCallRectorTest
*/
final class DowngradeClosureCallRector extends AbstractRector
{
public function __construct(
private MethodTypeAnalyzer $methodTypeAnalyzer
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Replace Closure::call() by Closure::bindTo()',
[
new CodeSample(
<<<'CODE_SAMPLE'
$closure->call($newObj, ...$args);
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
call_user_func($closure->bindTo($newObj, $newObj), ...$args);
CODE_SAMPLE
),
]
);
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [MethodCall::class];
}

/**
* @param MethodCall $node
*/
public function refactor(Node $node): ?FuncCall
{
if ($this->shouldSkip($node)) {
return null;
}

$methodCall = $this->createBindToCall($node);
$args = [new Arg($methodCall), ...array_slice($node->args, 1)];

return new FuncCall(new Name('call_user_func'), $args);
}

private function shouldSkip(MethodCall $methodCall): bool
{
if ($methodCall->args === []) {
return true;
}

return ! $this->methodTypeAnalyzer->isCallTo($methodCall, Closure::class, 'call');
}

private function createBindToCall(MethodCall $methodCall): MethodCall
{
$newObj = $methodCall->args[0];
if ($newObj->value instanceof Variable) {
$args = [$newObj, $newObj];
} else {
// we don't want the expression to be executed twice so we use array_fill() as a trick
$args = [new Arg(new LNumber(0)), new Arg(new LNumber(2)), $newObj];
$funcCall = new FuncCall(new Name('array_fill'), $args);
$args = [new Arg($funcCall, false, true)];
}

return new MethodCall($methodCall->var, 'bindTo', $args);
}
}

0 comments on commit 50a78a6

Please sign in to comment.