mirror of
https://github.com/rectorphp/rector.git
synced 2025-02-24 11:44:14 +01:00
Create ForeachToInArrayRector
This commit is contained in:
parent
d24aea7c85
commit
7d01962d6f
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
services:
|
||||
Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector: ~
|
@ -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#'
|
||||
|
@ -63,6 +63,14 @@ final class NodeFactory
|
||||
return BuilderHelpers::normalizeValue(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates "true"
|
||||
*/
|
||||
public function createTrueConstant(): ConstFetch
|
||||
{
|
||||
return BuilderHelpers::normalizeValue(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates "\SomeClass::CONSTANT"
|
||||
*/
|
||||
|
@ -36,6 +36,7 @@ final class ConstFetchAnalyzer
|
||||
if (! $node instanceof ConstFetch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $node->name->toLowerString() === $name;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user