[PHP] Add ArrayKeyFirstLastRector

This commit is contained in:
Tomas Votruba 2018-10-10 23:51:14 +08:00
parent bc658bee39
commit 2b2049c923
9 changed files with 232 additions and 0 deletions

View File

@ -1,2 +1,3 @@
services:
Rector\Php\Rector\BinaryOp\IsCountableRector: ~
Rector\Php\Rector\FuncCall\ArrayKeyFirstLastRector: ~

View File

@ -0,0 +1,162 @@
<?php declare(strict_types=1);
namespace Rector\Php\Rector\FuncCall;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PhpParser\Node\Stmt\Function_;
use Rector\Printer\BetterStandardPrinter;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see https://www.tomasvotruba.cz/blog/2018/08/16/whats-new-in-php-73-in-30-seconds-in-diffs/#2-first-and-last-array-key
*
* This needs to removed 1 floor above, because only nodes in arrays can be removed why traversing,
* see https://github.com/nikic/PHP-Parser/issues/389
*/
final class ArrayKeyFirstLastRector extends AbstractRector
{
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
/**
* @var string[]
*/
private $previousToNewFunctions = [
'reset' => 'array_key_first',
'end' => 'array_key_last',
];
public function __construct(BetterStandardPrinter $betterStandardPrinter)
{
$this->betterStandardPrinter = $betterStandardPrinter;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Make use of array_key_first() and array_key_last()',
[
new CodeSample(
<<<'CODE_SAMPLE'
reset($items);
$firstKey = key($items);
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$firstKey = array_key_first($items);
CODE_SAMPLE
),
new CodeSample(
<<<'CODE_SAMPLE'
end($items);
$lastKey = key($items);
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
$lastKey = array_key_last($items);
CODE_SAMPLE
),
]
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Function_::class, ClassMethod::class];
}
/**
* @param Function_|ClassMethod $functionLikeNode
*/
public function refactor(Node $functionLikeNode): ?Node
{
if ($functionLikeNode->stmts === null) {
return $functionLikeNode;
}
foreach ($functionLikeNode->stmts as $key => $stmt) {
/** @var Expression $stmt */
if (! $this->isFuncCallMatch($stmt->expr)) {
continue;
}
if (! isset($functionLikeNode->stmts[$key + 1])) {
continue;
}
if (! $this->isAssignMatch($functionLikeNode->stmts[$key + 1]->expr)) {
continue;
}
$funcCallNode = $stmt->expr;
/** @var FuncCall $funcCallNode */
$currentFuncCallName = (string) $funcCallNode->name;
/** @var Assign $assignNode */
$assignNode = $functionLikeNode->stmts[$key + 1]->expr;
/** @var FuncCall $assignFuncCallNode */
$assignFuncCallNode = $assignNode->expr;
if (! $this->areFuncCallNodesArgsEqual($funcCallNode, $assignFuncCallNode)) {
continue;
}
// rename next method to new one
$assignNode->expr->name = new Name($this->previousToNewFunctions[$currentFuncCallName]);
// remove unused node
unset($functionLikeNode->stmts[$key]);
}
// reindex for printer
$functionLikeNode->stmts = array_values($functionLikeNode->stmts);
return $functionLikeNode;
}
private function isFuncCallMatch(Node $node): bool
{
if (! $node instanceof FuncCall) {
return false;
}
return in_array((string) $node->name, array_keys($this->previousToNewFunctions), true);
}
private function isAssignMatch(Node $node): bool
{
if (! $node instanceof Assign) {
return false;
}
if (! $node->expr instanceof FuncCall) {
return false;
}
return (string) $node->expr->name === 'key';
}
private function areFuncCallNodesArgsEqual(FuncCall $firstFuncCallNode, FuncCall $secondFuncCallNode): bool
{
if (! isset($firstFuncCallNode->args[0]) || ! isset($secondFuncCallNode->args[0])) {
return false;
}
return $this->betterStandardPrinter->prettyPrint([$firstFuncCallNode->args[0]])
=== $this->betterStandardPrinter->prettyPrint([$secondFuncCallNode->args[0]]);
}
}

View File

@ -0,0 +1,31 @@
<?php declare(strict_types=1);
namespace Rector\Php\Tests\Rector\FuncCall\ArrayKeyFirstLastRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
/**
* @covers \Rector\Php\Rector\FuncCall\ArrayKeyFirstLastRector
*/
final class ArrayKeyFirstLastRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideWrongToFixedFiles()
*/
public function test(string $wrong, string $fixed): void
{
$this->doTestFileMatchesExpectedContent($wrong, $fixed);
}
public function provideWrongToFixedFiles(): Iterator
{
yield [__DIR__ . '/Wrong/wrong.php.inc', __DIR__ . '/Correct/correct.php.inc'];
yield [__DIR__ . '/Wrong/wrong2.php.inc', __DIR__ . '/Correct/correct2.php.inc'];
}
protected function provideConfig(): string
{
return __DIR__ . '/config.yml';
}
}

View File

@ -0,0 +1,9 @@
<?php
function process() {
$items = [1, 2, 3];
$firstKey = array_key_first($items);
reset($items);
$firstKey = key($differntItems);
}

View File

@ -0,0 +1,7 @@
<?php
function someFunction()
{
$item = [1, 2, 3];
$lastKey = array_key_last($items);
}

View File

@ -0,0 +1,10 @@
<?php
function process() {
$items = [1, 2, 3];
reset($items);
$firstKey = key($items);
reset($items);
$firstKey = key($differntItems);
}

View File

@ -0,0 +1,8 @@
<?php
function someFunction()
{
$item = [1, 2, 3];
end($items);
$lastKey = key($items);
}

View File

@ -0,0 +1,2 @@
services:
Rector\Php\Rector\FuncCall\ArrayKeyFirstLastRector: ~

View File

@ -86,6 +86,8 @@ parameters:
# buggy
- '#Access to an undefined property PhpParser\\Node\\Expr::\$value#' # 2
- '#Access to an undefined property PhpParser\\Node\\Expr::\$(name|var)#' # 2
- '#Access to an undefined property PhpParser\\Node\\Stmt::\$expr#'
- '#Binary operation "\+" between int\|string and 1 results in an error#'
# variadic false positive
- '#In method "Rector\\Node\\NodeFactory::createArray", parameter \$items can be type-hinted to "array"#'