diff --git a/config/level/php/php72.yml b/config/level/php/php72.yml new file mode 100644 index 00000000000..b40dd055c24 --- /dev/null +++ b/config/level/php/php72.yml @@ -0,0 +1,3 @@ +services: + Rector\Php\Rector\While_\WhileEachToForeachRector: ~ + Rector\Php\Rector\Each\ListEachRector: ~ diff --git a/packages/Php/src/Rector/Each/ListEachRector.php b/packages/Php/src/Rector/Each/ListEachRector.php new file mode 100644 index 00000000000..f3d2ad1d403 --- /dev/null +++ b/packages/Php/src/Rector/Each/ListEachRector.php @@ -0,0 +1,141 @@ +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_; + } +} diff --git a/packages/Php/src/Rector/Each/WhileEachToForeachRector.php b/packages/Php/src/Rector/Each/WhileEachToForeachRector.php new file mode 100644 index 00000000000..6fdfe12c474 --- /dev/null +++ b/packages/Php/src/Rector/Each/WhileEachToForeachRector.php @@ -0,0 +1,117 @@ + $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'; + } +} diff --git a/packages/Php/tests/Rector/Each/Correct/correct.php.inc b/packages/Php/tests/Rector/Each/Correct/correct.php.inc new file mode 100644 index 00000000000..dd1d991efaf --- /dev/null +++ b/packages/Php/tests/Rector/Each/Correct/correct.php.inc @@ -0,0 +1,14 @@ + $callback) { + // comment +} + +$module_list = ['a', 'b']; + +foreach (array_keys($module_list) as $module) { + echo $module; +} diff --git a/packages/Php/tests/Rector/Each/Correct/correct2.php.inc b/packages/Php/tests/Rector/Each/Correct/correct2.php.inc new file mode 100644 index 00000000000..7061b09c4b1 --- /dev/null +++ b/packages/Php/tests/Rector/Each/Correct/correct2.php.inc @@ -0,0 +1,9 @@ +option); +$val = current($opt->option); + +$tid = key($option->option); + +$curr = current($tree); +next($tree); diff --git a/packages/Php/tests/Rector/Each/Correct/correct3.php.inc b/packages/Php/tests/Rector/Each/Correct/correct3.php.inc new file mode 100644 index 00000000000..d96a5626d41 --- /dev/null +++ b/packages/Php/tests/Rector/Each/Correct/correct3.php.inc @@ -0,0 +1,10 @@ +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'; + } +} diff --git a/packages/Php/tests/Rector/Each/Wrong/wrong.php.inc b/packages/Php/tests/Rector/Each/Wrong/wrong.php.inc new file mode 100644 index 00000000000..ebcae7a88a7 --- /dev/null +++ b/packages/Php/tests/Rector/Each/Wrong/wrong.php.inc @@ -0,0 +1,14 @@ +option); + +list ($tid, ) = each($option->option); + +list(, $curr) = each($tree); diff --git a/packages/Php/tests/Rector/Each/Wrong/wrong3.php.inc b/packages/Php/tests/Rector/Each/Wrong/wrong3.php.inc new file mode 100644 index 00000000000..b53cad08da3 --- /dev/null +++ b/packages/Php/tests/Rector/Each/Wrong/wrong3.php.inc @@ -0,0 +1,8 @@ +