Downgrade arrow functions (#4137)

* Extracted rule into abstract class for reuse

* Implemented rule

* Added rule to downgrade set

* Fixed rector-ci errors

* Fixed cs

* Preserve return type

* Direct assignment of parameters

* Use interface instead of abstract methods

* Moved abstract class under Php72

* Added test for params with type
This commit is contained in:
Leonardo Losoviz 2020-09-07 21:38:03 +08:00 committed by GitHub
parent c6203e96d2
commit a0f99c2c7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 513 additions and 75 deletions

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
use Rector\Downgrade\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector;
use Rector\Downgrade\Rector\Coalesce\DowngradeNullCoalescingOperatorRector;
use Rector\Downgrade\Rector\FunctionLike\DowngradeParamObjectTypeDeclarationRector;
use Rector\Downgrade\Rector\FunctionLike\DowngradeReturnObjectTypeDeclarationRector;
@ -17,5 +18,7 @@ return static function (ContainerConfigurator $containerConfigurator): void {
$services->set(DowngradeTypedPropertyRector::class);
$services->set(ArrowFunctionToAnonymousFunctionRector::class);
$services->set(DowngradeNullCoalescingOperatorRector::class);
};

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Rector\Downgrade\Rector\ArrowFunction;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Return_;
use PhpParser\Node\UnionType;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\Php72\Rector\FuncCall\AbstractConvertToAnonymousFunctionRector;
/**
* @see https://www.php.net/manual/en/functions.arrow.php
*
* @see \Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\ArrowFunctionToAnonymousFunctionRectorTest
*/
final class ArrowFunctionToAnonymousFunctionRector extends AbstractConvertToAnonymousFunctionRector
{
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Replace arrow functions with anonymous functions', [
new CodeSample(
<<<'PHP'
class SomeClass
{
public function run()
{
$delimiter = ",";
$callable = fn($matches) => $delimiter . strtolower($matches[1]);
}
}
PHP
,
<<<'PHP'
class SomeClass
{
public function run()
{
$delimiter = ",";
$callable = function ($matches) use ($delimiter) {
return $delimiter . strtolower($matches[1]);
};
}
}
PHP
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [ArrowFunction::class];
}
/**
* @param ArrowFunction $node
*/
public function shouldSkip(Node $node): bool
{
return false;
}
/**
* @param ArrowFunction $node
* @return Param[]
*/
public function getParameters(Node $node): array
{
return $node->params;
}
/**
* @param ArrowFunction $node
* @return Identifier|Name|NullableType|UnionType|null
*/
public function getReturnType(Node $node): ?Node
{
return $node->returnType;
}
/**
* @param ArrowFunction $node
* @return Return_[]
*/
public function getBody(Node $node): array
{
return [new Return_($node->expr)];
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector;
use Iterator;
use Rector\Core\Testing\PHPUnit\AbstractRectorTestCase;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Downgrade\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector;
use Symplify\SmartFileSystem\SmartFileInfo;
final class ArrowFunctionToAnonymousFunctionRectorTest extends AbstractRectorTestCase
{
/**
* @requires PHP >= 7.4
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}
protected function getRectorClass(): string
{
return ArrowFunctionToAnonymousFunctionRector::class;
}
protected function getPhpVersion(): string
{
return PhpVersionFeature::BEFORE_ARROW_FUNCTION;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class FixtureClass
{
public function run()
{
$delimiter = ",";
$callable = fn($matches) => $delimiter . strtolower($matches[1]);
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class FixtureClass
{
public function run()
{
$delimiter = ",";
$callable = function ($matches) use ($delimiter) {
return $delimiter . strtolower($matches[1]);
};
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoParamsClass
{
public function run()
{
$delimiter = " ";
$callable = fn() => 'Hello' . $delimiter . 'world';
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoParamsClass
{
public function run()
{
$delimiter = " ";
$callable = function () use ($delimiter) {
return 'Hello' . $delimiter . 'world';
};
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoParamsOrUseClass
{
public function run()
{
$callable = fn() => 'Hello world';
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoParamsOrUseClass
{
public function run()
{
$callable = function () {
return 'Hello world';
};
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoUseClass
{
public function duplicate()
{
$numbers = [3, 5, 7];
return array_map(fn(int $number) => $number * 2, $numbers);
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNoUseClass
{
public function duplicate()
{
$numbers = [3, 5, 7];
return array_map(function (int $number) {
return $number * 2;
}, $numbers);
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class ParamsWithTypeClass
{
public function run()
{
$delimiter = ",";
$callable = fn(array $matches) => $delimiter . strtolower($matches[1]);
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class ParamsWithTypeClass
{
public function run()
{
$delimiter = ",";
$callable = function (array $matches) use ($delimiter) {
return $delimiter . strtolower($matches[1]);
};
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNullableReturnTypeClass
{
public function run()
{
$callable = fn(): ?string => 'Hello world';
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithNullableReturnTypeClass
{
public function run()
{
$callable = function () : ?string {
return 'Hello world';
};
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithReturnTypeClass
{
public function run()
{
$callable = fn(): string => 'Hello world';
}
}
?>
-----
<?php
namespace Rector\Downgrade\Tests\Rector\ArrowFunction\ArrowFunctionToAnonymousFunctionRector\Fixture;
class WithReturnTypeClass
{
public function run()
{
$callable = function () : string {
return 'Hello world';
};
}
}
?>

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace Rector\Php72\Contract;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\UnionType;
interface ConvertToAnonymousFunctionRectorInterface
{
public function shouldSkip(Node $node): bool;
/**
* @return Param[]
*/
public function getParameters(Node $node): array;
/**
* @return Identifier|Name|NullableType|UnionType|null
*/
public function getReturnType(Node $node): ?Node;
/**
* @return Expression[]|Stmt[]
*/
public function getBody(Node $node): array;
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Rector\Php72\Rector\FuncCall;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Param;
use Rector\Core\Rector\AbstractRector;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Php72\Contract\ConvertToAnonymousFunctionRectorInterface;
/**
* @see https://www.php.net/functions.anonymous
*/
abstract class AbstractConvertToAnonymousFunctionRector extends AbstractRector implements ConvertToAnonymousFunctionRectorInterface
{
public function refactor(Node $node): ?Node
{
if ($this->shouldSkip($node)) {
return null;
}
$body = $this->getBody($node);
$parameters = $this->getParameters($node);
$useVariables = $this->resolveUseVariables($body, $parameters);
$anonymousFunctionNode = new Closure();
$anonymousFunctionNode->params = $parameters;
foreach ($useVariables as $useVariable) {
$anonymousFunctionNode->uses[] = new ClosureUse($useVariable);
}
$anonymousFunctionNode->returnType = $this->getReturnType($node);
if ($body !== []) {
$anonymousFunctionNode->stmts = $body;
}
return $anonymousFunctionNode;
}
/**
* @param Node[] $nodes
* @param Param[] $paramNodes
* @return Variable[]
*/
private function resolveUseVariables(array $nodes, array $paramNodes): array
{
$paramNames = [];
foreach ($paramNodes as $paramNode) {
$paramNames[] = $this->getName($paramNode);
}
$variableNodes = $this->betterNodeFinder->findInstanceOf($nodes, Variable::class);
/** @var Variable[] $filteredVariables */
$filteredVariables = [];
$alreadyAssignedVariables = [];
foreach ($variableNodes as $variableNode) {
// "$this" is allowed
if ($this->isName($variableNode, 'this')) {
continue;
}
$variableName = $this->getName($variableNode);
if ($variableName === null) {
continue;
}
if (in_array($variableName, $paramNames, true)) {
continue;
}
$parentNode = $variableNode->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof Assign) {
$alreadyAssignedVariables[] = $variableName;
}
if ($this->isNames($variableNode, $alreadyAssignedVariables)) {
continue;
}
$filteredVariables[$variableName] = $variableNode;
}
return $filteredVariables;
}
}

View File

@ -10,21 +10,20 @@ use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp\Concat;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Scalar\Encapsed;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\UnionType;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\PhpParser\Parser\InlineCodeParser;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* @see https://stackoverflow.com/q/48161526/1348344
@ -32,7 +31,7 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
*
* @see \Rector\Php72\Tests\Rector\FuncCall\CreateFunctionToAnonymousFunctionRector\CreateFunctionToAnonymousFunctionRectorTest
*/
final class CreateFunctionToAnonymousFunctionRector extends AbstractRector
final class CreateFunctionToAnonymousFunctionRector extends AbstractConvertToAnonymousFunctionRector
{
/**
* @var InlineCodeParser
@ -84,33 +83,35 @@ PHP
/**
* @param FuncCall $node
*/
public function refactor(Node $node): ?Node
public function shouldSkip(Node $node): bool
{
if (! $this->isName($node, 'create_function')) {
return null;
}
return ! $this->isName($node, 'create_function');
}
/** @var Variable[] $parameters */
$parameters = $this->parseStringToParameters($node->args[0]->value);
$body = $this->parseStringToBody($node->args[1]->value);
$useVariables = $this->resolveUseVariables($body, $parameters);
/**
* @param FuncCall $node
* @return Param[]
*/
public function getParameters(Node $node): array
{
return $this->parseStringToParameters($node->args[0]->value);
}
$anonymousFunctionNode = new Closure();
/**
* @return Identifier|Name|NullableType|UnionType|null
*/
public function getReturnType(Node $node): ?Node
{
return null;
}
foreach ($parameters as $parameter) {
/** @var Variable $parameter */
$anonymousFunctionNode->params[] = new Param($parameter);
}
if ($body !== []) {
$anonymousFunctionNode->stmts = $body;
}
foreach ($useVariables as $useVariable) {
$anonymousFunctionNode->uses[] = new ClosureUse($useVariable);
}
return $anonymousFunctionNode;
/**
* @param FuncCall $node
* @return Stmt[]
*/
public function getBody(Node $node): array
{
return $this->parseStringToBody($node->args[1]->value);
}
/**
@ -153,53 +154,6 @@ PHP
return $this->inlineCodeParser->parse($node);
}
/**
* @param Node[] $nodes
* @param Variable[] $paramNodes
* @return Variable[]
*/
private function resolveUseVariables(array $nodes, array $paramNodes): array
{
$paramNames = [];
foreach ($paramNodes as $paramNode) {
$paramNames[] = $this->getName($paramNode);
}
$variableNodes = $this->betterNodeFinder->findInstanceOf($nodes, Variable::class);
/** @var Variable[] $filteredVariables */
$filteredVariables = [];
$alreadyAssignedVariables = [];
foreach ($variableNodes as $variableNode) {
// "$this" is allowed
if ($this->isName($variableNode, 'this')) {
continue;
}
$variableName = $this->getName($variableNode);
if ($variableName === null) {
continue;
}
if (in_array($variableName, $paramNames, true)) {
continue;
}
$parentNode = $variableNode->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof Assign) {
$alreadyAssignedVariables[] = $variableName;
}
if ($this->isNames($variableNode, $alreadyAssignedVariables)) {
continue;
}
$filteredVariables[$variableName] = $variableNode;
}
return $filteredVariables;
}
private function createEval(Expr $expr): Expression
{
$evalFuncCall = new FuncCall(new Name('eval'), [new Arg($expr)]);

View File

@ -146,6 +146,11 @@ final class PhpVersionFeature
*/
public const BEFORE_TYPED_PROPERTIES = '7.3';
/**
* @var string
*/
public const BEFORE_ARROW_FUNCTION = '7.3';
/**
* @var string
*/