mirror of
https://github.com/rectorphp/rector.git
synced 2025-01-18 22:08:00 +01:00
[PHP] Add Each Rectors
This commit is contained in:
parent
297773b281
commit
d1439079cb
3
config/level/php/php72.yml
Normal file
3
config/level/php/php72.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
services:
|
||||||
|
Rector\Php\Rector\While_\WhileEachToForeachRector: ~
|
||||||
|
Rector\Php\Rector\Each\ListEachRector: ~
|
141
packages/Php/src/Rector/Each/ListEachRector.php
Normal file
141
packages/Php/src/Rector/Each/ListEachRector.php
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Php\Rector\Each;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Arg;
|
||||||
|
use PhpParser\Node\Expr\Assign;
|
||||||
|
use PhpParser\Node\Expr\FuncCall;
|
||||||
|
use PhpParser\Node\Expr\List_;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt\Do_;
|
||||||
|
use PhpParser\Node\Stmt\Expression;
|
||||||
|
use Rector\NodeTypeResolver\Node\Attribute;
|
||||||
|
use Rector\Rector\AbstractRector;
|
||||||
|
use Rector\RectorDefinition\CodeSample;
|
||||||
|
use Rector\RectorDefinition\RectorDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @source https://wiki.php.net/rfc/deprecations_php_7_2#each
|
||||||
|
*/
|
||||||
|
final class ListEachRector extends AbstractRector
|
||||||
|
{
|
||||||
|
public function getDefinition(): RectorDefinition
|
||||||
|
{
|
||||||
|
return new RectorDefinition(
|
||||||
|
'each() function is deprecated, use foreach() instead.',
|
||||||
|
[
|
||||||
|
new CodeSample(
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
list($key, $callback) = each($callbacks);
|
||||||
|
CODE_SAMPLE
|
||||||
|
,
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
$key = key($opt->option);
|
||||||
|
$val = current($opt->option);
|
||||||
|
CODE_SAMPLE
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getNodeTypes(): array
|
||||||
|
{
|
||||||
|
return [Assign::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Assign $assignNode
|
||||||
|
*/
|
||||||
|
public function refactor(Node $assignNode): ?Node
|
||||||
|
{
|
||||||
|
if (! $this->isListToEachAssign($assignNode)) {
|
||||||
|
return $assignNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign should be top level, e.g. not in a while loop
|
||||||
|
if (! $assignNode->getAttribute(Attribute::PARENT_NODE) instanceof Expression) {
|
||||||
|
return $assignNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var List_ $listNode */
|
||||||
|
$listNode = $assignNode->var;
|
||||||
|
if (count($listNode->items) !== 2) {
|
||||||
|
return $listNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var FuncCall $eachFuncCall */
|
||||||
|
$eachFuncCall = $assignNode->expr;
|
||||||
|
|
||||||
|
// only key: list($key, ) = each($values);
|
||||||
|
if ($listNode->items[0] && $listNode->items[1] === null) {
|
||||||
|
$keyFuncCall = $this->createFuncCallWithNameAndArgs('key', $eachFuncCall->args);
|
||||||
|
return new Assign($listNode->items[0]->value, $keyFuncCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only value: list(, $value) = each($values);
|
||||||
|
if ($listNode->items[1] && $listNode->items[0] === null) {
|
||||||
|
$nextFuncCall = $this->createFuncCallWithNameAndArgs('next', $eachFuncCall->args);
|
||||||
|
$this->addNodeAfterNode($nextFuncCall, $assignNode);
|
||||||
|
|
||||||
|
$currentFuncCall = $this->createFuncCallWithNameAndArgs('current', $eachFuncCall->args);
|
||||||
|
return new Assign($listNode->items[1]->value, $currentFuncCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
// both: list($key, $value) = each($values);
|
||||||
|
// ↓
|
||||||
|
// $key = key($values);
|
||||||
|
// $value = current($values);
|
||||||
|
// next($values); - only inside a loop
|
||||||
|
if ($listNode->items[0] && $listNode->items[1]) {
|
||||||
|
$currentFuncCall = $this->createFuncCallWithNameAndArgs('current', $eachFuncCall->args);
|
||||||
|
$assignCurrentNode = new Assign($listNode->items[1]->value, $currentFuncCall);
|
||||||
|
$this->addNodeAfterNode($assignCurrentNode, $assignNode);
|
||||||
|
|
||||||
|
if ($this->isInsideDoWhile($assignNode)) {
|
||||||
|
$nextFuncCall = $this->createFuncCallWithNameAndArgs('next', $eachFuncCall->args);
|
||||||
|
$this->addNodeAfterNode($nextFuncCall, $assignNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$keyFuncCall = $this->createFuncCallWithNameAndArgs('key', $eachFuncCall->args);
|
||||||
|
return new Assign($listNode->items[0]->value, $keyFuncCall);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $assignNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isListToEachAssign(Assign $assignNode): bool
|
||||||
|
{
|
||||||
|
if (! $assignNode->var instanceof List_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $assignNode->expr instanceof FuncCall && (string) $assignNode->expr->name === 'each';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Arg[] $args
|
||||||
|
*/
|
||||||
|
private function createFuncCallWithNameAndArgs(string $name, array $args): FuncCall
|
||||||
|
{
|
||||||
|
return new FuncCall(new Name($name), $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is inside the "do {} while ();" loop → need to add "next()"
|
||||||
|
*/
|
||||||
|
private function isInsideDoWhile(Node $assignNode): bool
|
||||||
|
{
|
||||||
|
$parentNode = $assignNode->getAttribute(Attribute::PARENT_NODE);
|
||||||
|
if (! $parentNode instanceof Expression) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentParentNode = $parentNode->getAttribute(Attribute::PARENT_NODE);
|
||||||
|
|
||||||
|
return $parentParentNode instanceof Do_;
|
||||||
|
}
|
||||||
|
}
|
117
packages/Php/src/Rector/Each/WhileEachToForeachRector.php
Normal file
117
packages/Php/src/Rector/Each/WhileEachToForeachRector.php
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Php\Rector\Each;
|
||||||
|
|
||||||
|
use PhpParser\Node;
|
||||||
|
use PhpParser\Node\Expr\ArrayItem;
|
||||||
|
use PhpParser\Node\Expr\Assign;
|
||||||
|
use PhpParser\Node\Expr\FuncCall;
|
||||||
|
use PhpParser\Node\Expr\List_;
|
||||||
|
use PhpParser\Node\Name;
|
||||||
|
use PhpParser\Node\Stmt\Foreach_;
|
||||||
|
use PhpParser\Node\Stmt\While_;
|
||||||
|
use Rector\Rector\AbstractRector;
|
||||||
|
use Rector\RectorDefinition\CodeSample;
|
||||||
|
use Rector\RectorDefinition\RectorDefinition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @source https://wiki.php.net/rfc/deprecations_php_7_2#each
|
||||||
|
*/
|
||||||
|
final class WhileEachToForeachRector extends AbstractRector
|
||||||
|
{
|
||||||
|
public function getDefinition(): RectorDefinition
|
||||||
|
{
|
||||||
|
return new RectorDefinition(
|
||||||
|
'each() function is deprecated, use foreach() instead.',
|
||||||
|
[
|
||||||
|
new CodeSample(
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
while (list($key, $callback) = each($callbacks)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
CODE_SAMPLE
|
||||||
|
,
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
foreach ($callbacks as $key => $callback) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
CODE_SAMPLE
|
||||||
|
),
|
||||||
|
new CodeSample(
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
while (list($key) = each($callbacks)) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
CODE_SAMPLE
|
||||||
|
,
|
||||||
|
<<<'CODE_SAMPLE'
|
||||||
|
foreach (array_keys($callbacks) as $key) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
CODE_SAMPLE
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getNodeTypes(): array
|
||||||
|
{
|
||||||
|
return [While_::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param While_ $whileNode
|
||||||
|
*/
|
||||||
|
public function refactor(Node $whileNode): ?Node
|
||||||
|
{
|
||||||
|
if (! $whileNode->cond instanceof Assign) {
|
||||||
|
return $whileNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Assign $assignNode */
|
||||||
|
$assignNode = $whileNode->cond;
|
||||||
|
if (! $this->isListToEachAssign($assignNode)) {
|
||||||
|
return $whileNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var FuncCall $eachFuncCall */
|
||||||
|
$eachFuncCall = $assignNode->expr;
|
||||||
|
$eachFuncCall->args[0];
|
||||||
|
|
||||||
|
/** @var List_ $listNode */
|
||||||
|
$listNode = $assignNode->var;
|
||||||
|
|
||||||
|
if (count($listNode->items) === 1) { // just one argument - the key
|
||||||
|
$foreachedExpr = new FuncCall(new Name('array_keys'), [$eachFuncCall->args[0]]);
|
||||||
|
} else {
|
||||||
|
$foreachedExpr = $eachFuncCall->args[0]->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ArrayItem $valueItem */
|
||||||
|
$valueItem = array_pop($listNode->items);
|
||||||
|
$foreachNode = new Foreach_($foreachedExpr, $valueItem, [
|
||||||
|
'stmts' => $whileNode->stmts,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// is key included? add it to foreach
|
||||||
|
if (count($listNode->items)) {
|
||||||
|
/** @var ArrayItem $keyItem */
|
||||||
|
$keyItem = array_pop($listNode->items);
|
||||||
|
$foreachNode->keyVar = $keyItem->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $foreachNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isListToEachAssign(Assign $assignNode): bool
|
||||||
|
{
|
||||||
|
if (! $assignNode->var instanceof List_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $assignNode->expr instanceof FuncCall && (string) $assignNode->expr->name === 'each';
|
||||||
|
}
|
||||||
|
}
|
14
packages/Php/tests/Rector/Each/Correct/correct.php.inc
Normal file
14
packages/Php/tests/Rector/Each/Correct/correct.php.inc
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
foreach (array_keys($module_list) as $module) {
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($callbacks as $key => $callback) {
|
||||||
|
// comment
|
||||||
|
}
|
||||||
|
|
||||||
|
$module_list = ['a', 'b'];
|
||||||
|
|
||||||
|
foreach (array_keys($module_list) as $module) {
|
||||||
|
echo $module;
|
||||||
|
}
|
9
packages/Php/tests/Rector/Each/Correct/correct2.php.inc
Normal file
9
packages/Php/tests/Rector/Each/Correct/correct2.php.inc
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
$key = key($opt->option);
|
||||||
|
$val = current($opt->option);
|
||||||
|
|
||||||
|
$tid = key($option->option);
|
||||||
|
|
||||||
|
$curr = current($tree);
|
||||||
|
next($tree);
|
10
packages/Php/tests/Rector/Each/Correct/correct3.php.inc
Normal file
10
packages/Php/tests/Rector/Each/Correct/correct3.php.inc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
$flat = book_get_flat_menu($book_link);
|
||||||
|
$curr = NULL;
|
||||||
|
do {
|
||||||
|
$prev = $curr;
|
||||||
|
$key = key($flat);
|
||||||
|
$curr = current($flat);
|
||||||
|
next($flat);
|
||||||
|
} while ($key && $key != $book_link['mlid']);
|
39
packages/Php/tests/Rector/Each/EachRectorTest.php
Normal file
39
packages/Php/tests/Rector/Each/EachRectorTest.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Rector\Php\Tests\Rector\Each;
|
||||||
|
|
||||||
|
use Iterator;
|
||||||
|
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \Rector\Php\Rector\Each\WhileEachToForeachRector
|
||||||
|
* @covers \Rector\Php\Rector\Each\ListEachRector
|
||||||
|
*
|
||||||
|
* Test battery inspired by:
|
||||||
|
* - https://stackoverflow.com/q/46492621/1348344 + Drupal refactorings
|
||||||
|
* - https://stackoverflow.com/a/51278641/1348344
|
||||||
|
*/
|
||||||
|
final class EachRectorTest extends AbstractRectorTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @dataProvider provideWrongToFixedFiles()
|
||||||
|
*/
|
||||||
|
public function test(string $wrong, string $fixed): void
|
||||||
|
{
|
||||||
|
$this->doTestFileMatchesExpectedContent($wrong, $fixed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideWrongToFixedFiles(): Iterator
|
||||||
|
{
|
||||||
|
// while → foreach
|
||||||
|
yield [__DIR__ . '/Wrong/wrong.php.inc', __DIR__ . '/Correct/correct.php.inc'];
|
||||||
|
// list()
|
||||||
|
yield [__DIR__ . '/Wrong/wrong2.php.inc', __DIR__ . '/Correct/correct2.php.inc'];
|
||||||
|
yield [__DIR__ . '/Wrong/wrong3.php.inc', __DIR__ . '/Correct/correct3.php.inc'];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function provideConfig(): string
|
||||||
|
{
|
||||||
|
return __DIR__ . '/config.yml';
|
||||||
|
}
|
||||||
|
}
|
14
packages/Php/tests/Rector/Each/Wrong/wrong.php.inc
Normal file
14
packages/Php/tests/Rector/Each/Wrong/wrong.php.inc
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
while (list($module) = each($module_list)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
while (list($key, $callback) = each($callbacks)) {
|
||||||
|
// comment
|
||||||
|
}
|
||||||
|
|
||||||
|
$module_list = ['a', 'b'];
|
||||||
|
|
||||||
|
while (list($module) = each($module_list)) {
|
||||||
|
echo $module;
|
||||||
|
}
|
7
packages/Php/tests/Rector/Each/Wrong/wrong2.php.inc
Normal file
7
packages/Php/tests/Rector/Each/Wrong/wrong2.php.inc
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
list($key, $val) = each($opt->option);
|
||||||
|
|
||||||
|
list ($tid, ) = each($option->option);
|
||||||
|
|
||||||
|
list(, $curr) = each($tree);
|
8
packages/Php/tests/Rector/Each/Wrong/wrong3.php.inc
Normal file
8
packages/Php/tests/Rector/Each/Wrong/wrong3.php.inc
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
$flat = book_get_flat_menu($book_link);
|
||||||
|
$curr = NULL;
|
||||||
|
do {
|
||||||
|
$prev = $curr;
|
||||||
|
list($key, $curr) = each($flat);
|
||||||
|
} while ($key && $key != $book_link['mlid']);
|
3
packages/Php/tests/Rector/Each/config.yml
Normal file
3
packages/Php/tests/Rector/Each/config.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
services:
|
||||||
|
Rector\Php\Rector\Each\WhileEachToForeachRector: ~
|
||||||
|
Rector\Php\Rector\Each\ListEachRector: ~
|
Loading…
x
Reference in New Issue
Block a user