Create ForeachToInArrayRector

This commit is contained in:
Gabriel Caruso 2018-10-12 17:19:34 -03:00
parent d24aea7c85
commit 7d01962d6f
No known key found for this signature in database
GPG Key ID: EA85C7988F5A6877
8 changed files with 393 additions and 0 deletions

View File

@ -0,0 +1,179 @@
<?php declare(strict_types=1);
namespace Rector\CodeQuality\Rector\Foreach_;
use PhpParser\Node;
use PhpParser\Node\Expr\BinaryOp\Equal;
use PhpParser\Node\Expr\BinaryOp\Identical;
use PhpParser\Node\Expr\BooleanNot;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\Return_;
use Rector\Node\NodeFactory;
use Rector\NodeAnalyzer\ConstFetchAnalyzer;
use Rector\NodeTypeResolver\Node\Attribute;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
final class ForeachToInArrayRector extends AbstractRector
{
/**
* @var NodeFactory
*/
private $nodeFactory;
/**
* @var ConstFetchAnalyzer
*/
private $constFetchAnalyzer;
/**
* @var Return_
*/
private $returnNodeToRemove;
public function __construct(NodeFactory $nodeFactory, ConstFetchAnalyzer $constFetchAnalyzer)
{
$this->nodeFactory = $nodeFactory;
$this->constFetchAnalyzer = $constFetchAnalyzer;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition(
'Simplify `foreach` loops into `in_array` when possible',
[
new CodeSample(
<<<'CODE_SAMPLE'
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
return false;
CODE_SAMPLE
,
"in_array('something', \$items, true);"
),
]
);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Foreach_::class];
}
/**
* @param Foreach_ $foreachNode
*/
public function refactor(Node $foreachNode): ?Node
{
if (! $this->isAForeachCandidate($foreachNode)) {
return null;
}
$firstNodeInsideForeach = $foreachNode->stmts[0];
if (! $firstNodeInsideForeach instanceof If_) {
return null;
}
$ifCondition = $firstNodeInsideForeach->cond;
if (! $ifCondition instanceof Identical && ! $ifCondition instanceof Equal) {
return null;
}
$leftVariable = $ifCondition->left;
$rightVariable = $ifCondition->right;
if (! $leftVariable instanceof Variable & ! $rightVariable instanceof Variable) {
return null;
}
$condition = $this->normalizeYodaComparison($leftVariable, $rightVariable, $foreachNode);
if (! $this->isIfBodyABoolReturnNode($firstNodeInsideForeach)) {
return null;
}
$inArrayFunctionCall = $this->createInArrayFunction($condition, $ifCondition, $foreachNode);
$this->returnNodeToRemove = $foreachNode->getAttribute(Attribute::NEXT_NODE);
$this->removeNode($this->returnNodeToRemove);
$negativeReturn = $this->constFetchAnalyzer->isFalse($firstNodeInsideForeach->stmts[0]->expr);
return new Return_($negativeReturn ? new BooleanNot($inArrayFunctionCall) : $inArrayFunctionCall);
}
/**
* @param mixed $leftValue
* @param mixed $rightValue
*
* @return mixed
*/
private function normalizeYodaComparison($leftValue, $rightValue, Foreach_ $foreachNode)
{
if ($leftValue instanceof Variable) {
if ($leftValue->name === $foreachNode->valueVar->name) {
return $rightValue;
}
}
if ($rightValue->name === $foreachNode->valueVar->name) {
return $leftValue;
}
}
/**
* @param mixed $condition
* @param Identical|Equal $ifCondition
*/
private function createInArrayFunction($condition, $ifCondition, Foreach_ $foreachNode): FuncCall
{
$arguments = $this->nodeFactory->createArgs([$condition, $foreachNode->expr]);
if ($ifCondition instanceof Identical) {
$arguments[] = $this->nodeFactory->createArg($this->nodeFactory->createTrueConstant());
}
return new FuncCall(new Name('in_array'), $arguments);
}
private function isIfBodyABoolReturnNode(If_ $firstNodeInsideForeach): bool
{
$ifStatment = $firstNodeInsideForeach->stmts[0];
if (! $ifStatment instanceof Return_) {
return false;
}
return $this->constFetchAnalyzer->isBool($ifStatment->expr);
}
private function isAForeachCandidate(Foreach_ $foreachNode): bool
{
if (isset($foreachNode->keyVar)) {
return false;
}
$nextNode = $foreachNode->getAttribute(Attribute::NEXT_NODE);
if ($nextNode === null) {
return false;
}
$nextNode = $nextNode->expr;
return $nextNode !== null && $this->constFetchAnalyzer->isBool($nextNode);
}
}

View File

@ -0,0 +1,71 @@
<?php declare(strict_types=1);
final class MyClass
{
public function foreachToInArray() : bool
{
return in_array('something', $items);
}
public function foreachToInArrayYoda() : bool
{
return in_array('something', $items);
}
public function foreachToInArrayStrict() : bool
{
return in_array('something', $items, true);
}
public function invertedForeachToInArrayStrict() : bool
{
return !in_array('something', $items, true);
}
public function foreachToInArrayWithToVariables() : bool
{
return in_array($something, $items, true);
}
public function foreachWithoutReturnFalse()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
}
public function foreachReturnString()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
return 'false';
}
public function foreachWithSomethingElseAfterIt()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
$foo = 'bar';
}
public function foreachWithElseNullable()
{
foreach ($items as $item) {
if ('string') {
return true;
}
}
return;
}
}

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace Rector\CodeQuality\Tests\Rector\Foreach_\ForeachToInArrayRector;
use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
/**
* @covers \Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector
*/
final class ForeachToInArrayRectorTest 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'];
}
protected function provideConfig(): string
{
return __DIR__ . '/config.yml';
}
}

View File

@ -0,0 +1,101 @@
<?php declare(strict_types=1);
final class MyClass
{
public function foreachToInArray() : bool
{
foreach ($items as $item) {
if ($item == 'something') {
return true;
}
}
return false;
}
public function foreachToInArrayYoda() : bool
{
foreach ($items as $item) {
if ('something' == $item) {
return true;
}
}
return false;
}
public function foreachToInArrayStrict() : bool
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
return false;
}
public function invertedForeachToInArrayStrict() : bool
{
foreach ($items as $item) {
if ($item === 'something') {
return false;
}
}
return true;
}
public function foreachToInArrayWithToVariables() : bool
{
foreach ($items as $item) {
if ($something === $item) {
return true;
}
}
return false;
}
public function foreachWithoutReturnFalse()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
}
public function foreachReturnString()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
return 'false';
}
public function foreachWithSomethingElseAfterIt()
{
foreach ($items as $item) {
if ($item === 'something') {
return true;
}
}
$foo = 'bar';
}
public function foreachWithElseNullable()
{
foreach ($items as $item) {
if ('string') {
return true;
}
}
return;
}
}

View File

@ -0,0 +1,2 @@
services:
Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector: ~

View File

@ -54,6 +54,7 @@ parameters:
- '#Parameter \#1 \$classReflection of method Rector\\NodeTypeResolver\\Reflection\\ClassReflectionTypesResolver::resolve\(\) expects PHPStan\\Reflection\\ClassReflection, PHPStan\\Reflection\\ClassReflection|null given#'
- '#Cannot call method getAttribute\(\) on PhpParser\\Node\\Name\|null#'
- '#Cannot call method getText\(\) on PhpParser\\Comment\\Doc\|null#'
- '#Parameter \#2 \$args of class PhpParser\\Node\\Expr\\FuncCall constructor expects array<PhpParser\\Node\\Arg>, array<int, PhpParser\\Node\\Expr> given.#' #1
# false positive, has annotation type above
- '#Parameter \#1 \$node of method Rector\\PHPUnit\\Rector\\Foreach_\\SimplifyForeachInstanceOfRector\:\:resolveVar\(\) expects PhpParser\\Node\\Expr\\MethodCall\|PhpParser\\Node\\Expr\\StaticCall, PhpParser\\Node given#'

View File

@ -63,6 +63,14 @@ final class NodeFactory
return BuilderHelpers::normalizeValue(null);
}
/**
* Creates "true"
*/
public function createTrueConstant(): ConstFetch
{
return BuilderHelpers::normalizeValue(true);
}
/**
* Creates "\SomeClass::CONSTANT"
*/

View File

@ -36,6 +36,7 @@ final class ConstFetchAnalyzer
if (! $node instanceof ConstFetch) {
return false;
}
return $node->name->toLowerString() === $name;
}
}