[TypeDeclaration] Add AddFunctionReturnTypeRector

This commit is contained in:
Tomas Votruba 2019-05-09 15:30:39 +02:00
parent 548d13276e
commit ca289def87
12 changed files with 264 additions and 5 deletions

View File

@ -11,7 +11,7 @@
"jetbrains/phpstorm-stubs": "^2019.1",
"nette/robot-loader": "^3.1",
"nette/utils": "^2.5|^3.0",
"nikic/php-parser": "^4.2.1",
"nikic/php-parser": "dev-master",
"phpstan/phpdoc-parser": "0.3.3",
"phpstan/phpstan": "0.11.6",
"sebastian/diff": "^3.0",

View File

@ -16,3 +16,4 @@ services:
Rector\Php\Rector\Double\RealToFloatTypeCastRector: ~
Rector\Php\Rector\Assign\NullCoalescingOperatorRector: ~
Rector\Php\Rector\Function_\ReservedFnFunctionRector: ~
Rector\Php\Rector\Closure\ClosureToArrowFunctionRector: ~

View File

@ -176,6 +176,11 @@ final class DumpNodesCommand extends AbstractCommand
if ($contructorReflection->getNumberOfRequiredParameters() === 0) {
$node = $nodeClassReflection->newInstance();
// special case
if ($node instanceof Node\Expr\ArrowFunction) {
$node->expr = new LNumber(1);
}
$category = $this->resolveCategoryByNodeClass($nodeClass);
$this->nodeInfoResult->addNodeInfo($category, new NodeInfo(
$nodeClass,

View File

@ -0,0 +1,134 @@
<?php declare(strict_types=1);
namespace Rector\Php\Rector\Closure;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\ClosureUse;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Return_;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://wiki.php.net/rfc/arrow_functions_v2
*/
final class ClosureToArrowFunctionRector extends AbstractRector
{
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change closure to arrow function', [
new CodeSample(
<<<'CODE_SAMPLE'
class SomeClass
{
public function run($meetups)
{
return array_filter($meetups, function (Meetup $meetup) {
return is_object($meetup);
});
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
class SomeClass
{
public function run($meetups)
{
return array_filter($meetups, fn(Meetup $meetup) => is_object($meetup));
}
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Closure::class];
}
/**
* @param Closure $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->isAtLeastPhpVersion('7.4')) {
return null;
}
if (count((array) $node->stmts) !== 1) {
return null;
}
if (! $node->stmts[0] instanceof Return_) {
return null;
}
/** @var Return_ $return */
$return = $node->stmts[0];
if ($return->expr === null) {
return null;
}
if ($this->shouldSkipForUsedReferencedValue($node, $return)) {
return null;
}
$arrowFunction = new ArrowFunction();
$arrowFunction->params = $node->params;
$arrowFunction->returnType = $node->returnType;
$arrowFunction->byRef = $node->byRef;
$arrowFunction->expr = $return->expr;
return $arrowFunction;
}
private function shouldSkipForUsedReferencedValue(Closure $closure, Return_ $return): bool
{
if ($return->expr === null) {
return false;
}
$referencedValues = $this->resolveReferencedUseVariablesFromClosure($closure);
if ($referencedValues === []) {
return false;
}
return (bool) $this->betterNodeFinder->findFirst([$return->expr], function (Node $node) use (
$referencedValues
): bool {
foreach ($referencedValues as $referencedValue) {
if ($this->areNodesEqual($node, $referencedValue)) {
return true;
}
}
return false;
});
}
/**
* @return Variable[]
*/
private function resolveReferencedUseVariablesFromClosure(Closure $closure): array
{
$referencedValues = [];
/** @var ClosureUse $use */
foreach ((array) $closure->uses as $use) {
if ($use->byRef) {
$referencedValues[] = $use->var;
}
}
return $referencedValues;
}
}

View File

@ -0,0 +1,25 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector;
use Rector\Php\Rector\Closure\ClosureToArrowFunctionRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class ClosureToArrowFunctionRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/referenced_but_not_used.php.inc',
// skip
__DIR__ . '/Fixture/skip_no_return.php.inc',
__DIR__ . '/Fixture/skip_referenced_value.php.inc',
]);
}
protected function getRectorClass(): string
{
return ClosureToArrowFunctionRector::class;
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class SomeClass
{
public function run($meetups)
{
return array_filter($meetups, function (Meetup $meetup) {
return is_object($meetup);
});
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class SomeClass
{
public function run($meetups)
{
return array_filter($meetups, fn(Meetup $meetup) => is_object($meetup));
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class ReferencedButNotUsed
{
public function run()
{
$callback = function($b) use(&$i) {
return ++$b;
};
}
}
?>
-----
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class ReferencedButNotUsed
{
public function run()
{
$callback = fn($b) => ++$b;
}
}
?>

View File

@ -0,0 +1,17 @@
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class SkipNoReturn
{
public function run($meetups)
{
return array_filter($meetups, function (Meetup $meetup) {
is_object($meetup);
});
return array_filter($meetups, function (Meetup $meetup) {
return;
});
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace Rector\Php\Tests\Rector\Closure\ClosureToArrowFunctionRector\Fixture;
class SkipReferencedValue
{
public function run()
{
$callback = function() use(&$i) {
return ++$i;
};
}
}

View File

@ -2,6 +2,7 @@
namespace Rector\Php\Tests\Rector\Function_\ReservedFnFunctionRector;
use PhpParser\Parser\Tokens;
use Rector\Php\Rector\Function_\ReservedFnFunctionRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
@ -9,6 +10,10 @@ final class ReservedFnFunctionRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
if (defined(Tokens::class . '::T_FN')) {
$this->markTestSkipped('fn is reserved name in PHP 7.4');
}
$this->doTestFiles([__DIR__ . '/Fixture/fixture.php.inc']);
}

View File

@ -3,6 +3,7 @@
namespace Rector\TypeDeclaration\Rector\Closure;
use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PHPStan\Analyser\Scope;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\Manipulator\FunctionLikeManipulator;
@ -58,11 +59,11 @@ CODE_SAMPLE
*/
public function getNodeTypes(): array
{
return [Node\Expr\Closure::class];
return [Closure::class];
}
/**
* @param Node\Expr\Closure $node
* @param Closure $node
*/
public function refactor(Node $node): ?Node
{

View File

@ -15,5 +15,5 @@ parameters:
php_version_features: '7.1'
services:
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector: ~
Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
# Rector\TypeDeclaration\Rector\Closure\AddClosureReturnTypeRector: ~