rector/rules/CodeQuality/Rector/For_/ForToForeachRector.php

241 lines
7.9 KiB
PHP

<?php
declare (strict_types=1);
namespace Rector\CodeQuality\Rector\For_;
use RectorPrefix202302\Doctrine\Inflector\Inflector;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\BinaryOp;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use Rector\CodeQuality\NodeAnalyzer\ForAnalyzer;
use Rector\CodeQuality\NodeAnalyzer\ForeachAnalyzer;
use Rector\CodeQuality\NodeAnalyzer\VariableNameUsedNextAnalyzer;
use Rector\CodeQuality\NodeFactory\ForeachFactory;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
/**
* @see \Rector\Tests\CodeQuality\Rector\For_\ForToForeachRector\ForToForeachRectorTest
*/
final class ForToForeachRector extends AbstractRector
{
/**
* @var string
*/
private const COUNT = 'count';
/**
* @var string|null
*/
private $keyValueName;
/**
* @var string|null
*/
private $countValueName;
/**
* @var \PhpParser\Node\Expr|null
*/
private $countValueVariableExpr;
/**
* @var \PhpParser\Node\Expr|null
*/
private $iteratedExpr;
/**
* @readonly
* @var \Doctrine\Inflector\Inflector
*/
private $inflector;
/**
* @readonly
* @var \Rector\CodeQuality\NodeAnalyzer\ForAnalyzer
*/
private $forAnalyzer;
/**
* @readonly
* @var \Rector\CodeQuality\NodeFactory\ForeachFactory
*/
private $foreachFactory;
/**
* @readonly
* @var \Rector\CodeQuality\NodeAnalyzer\ForeachAnalyzer
*/
private $foreachAnalyzer;
/**
* @readonly
* @var \Rector\CodeQuality\NodeAnalyzer\VariableNameUsedNextAnalyzer
*/
private $variableNameUsedNextAnalyzer;
public function __construct(Inflector $inflector, ForAnalyzer $forAnalyzer, ForeachFactory $foreachFactory, ForeachAnalyzer $foreachAnalyzer, VariableNameUsedNextAnalyzer $variableNameUsedNextAnalyzer)
{
$this->inflector = $inflector;
$this->forAnalyzer = $forAnalyzer;
$this->foreachFactory = $foreachFactory;
$this->foreachAnalyzer = $foreachAnalyzer;
$this->variableNameUsedNextAnalyzer = $variableNameUsedNextAnalyzer;
}
public function getRuleDefinition() : RuleDefinition
{
return new RuleDefinition('Change for() to foreach() where useful', [new CodeSample(<<<'CODE_SAMPLE'
class SomeClass
{
public function run($tokens)
{
for ($i = 0, $c = count($tokens); $i < $c; ++$i) {
if ($tokens[$i][0] === T_STRING && $tokens[$i][1] === 'fn') {
$tokens[$i][0] = self::T_FN;
}
}
}
}
CODE_SAMPLE
, <<<'CODE_SAMPLE'
class SomeClass
{
public function run($tokens)
{
foreach ($tokens as $i => $token) {
if ($token[0] === T_STRING && $token[1] === 'fn') {
$tokens[$i][0] = self::T_FN;
}
}
}
}
CODE_SAMPLE
)]);
}
/**
* @return array<class-string<Node>>
*/
public function getNodeTypes() : array
{
return [For_::class];
}
/**
* @param For_ $node
*/
public function refactor(Node $node) : ?Node
{
$this->reset();
$this->matchInit($node->init);
if (!$this->isConditionMatch($node->cond)) {
return null;
}
if (!$this->forAnalyzer->isLoopMatch($node->loop, $this->keyValueName)) {
return null;
}
if ($this->iteratedExpr === null) {
return null;
}
if ($this->keyValueName === null) {
return null;
}
$iteratedVariable = $this->getName($this->iteratedExpr);
if ($iteratedVariable === null) {
return null;
}
$init = $node->init;
if (\count($init) > 2) {
return null;
}
if ($this->forAnalyzer->isCountValueVariableUsedInsideForStatements($node, $this->countValueVariableExpr)) {
return null;
}
if ($this->forAnalyzer->isAssignmentWithArrayDimFetchAsVariableInsideForStatements($node, $this->keyValueName)) {
return null;
}
if ($this->forAnalyzer->isArrayWithKeyValueNameUnsetted($node)) {
return null;
}
return $this->processForToForeach($node, $iteratedVariable);
}
private function processForToForeach(For_ $for, string $iteratedVariable) : ?Foreach_
{
$originalVariableSingle = $this->inflector->singularize($iteratedVariable);
$iteratedVariableSingle = $originalVariableSingle;
if ($iteratedVariableSingle === $iteratedVariable) {
$iteratedVariableSingle = 'single' . \ucfirst($iteratedVariableSingle);
}
if (!$this->variableNameUsedNextAnalyzer->isValueVarUsedNext($for, $iteratedVariableSingle)) {
return $this->createForeachFromForWithIteratedVariableSingle($for, $iteratedVariableSingle);
}
if ($iteratedVariableSingle === $originalVariableSingle) {
return null;
}
if (!$this->variableNameUsedNextAnalyzer->isValueVarUsedNext($for, $originalVariableSingle)) {
return $this->createForeachFromForWithIteratedVariableSingle($for, $originalVariableSingle);
}
return null;
}
private function createForeachFromForWithIteratedVariableSingle(For_ $for, string $iteratedVariableSingle) : Foreach_
{
$foreach = $this->foreachFactory->createFromFor($for, $iteratedVariableSingle, $this->iteratedExpr, $this->keyValueName);
$this->mirrorComments($foreach, $for);
if ($this->keyValueName === null) {
throw new ShouldNotHappenException();
}
$this->foreachAnalyzer->useForeachVariableInStmts($foreach->expr, $foreach->valueVar, $foreach->stmts, $this->keyValueName);
return $foreach;
}
private function reset() : void
{
$this->keyValueName = null;
$this->countValueVariableExpr = null;
$this->countValueName = null;
$this->iteratedExpr = null;
}
/**
* @param Expr[] $initExprs
*/
private function matchInit(array $initExprs) : void
{
foreach ($initExprs as $initExpr) {
if (!$initExpr instanceof Assign) {
continue;
}
if ($this->valueResolver->isValue($initExpr->expr, 0)) {
$this->keyValueName = $this->getName($initExpr->var);
}
if (!$initExpr->expr instanceof FuncCall) {
continue;
}
$funcCall = $initExpr->expr;
if ($this->nodeNameResolver->isName($funcCall, self::COUNT) && $funcCall->args[0] instanceof Arg) {
$this->countValueVariableExpr = $initExpr->var;
$this->countValueName = $this->getName($initExpr->var);
$this->iteratedExpr = $funcCall->args[0]->value;
}
}
}
/**
* @param Expr[] $condExprs
*/
private function isConditionMatch(array $condExprs) : bool
{
if ($this->forAnalyzer->isCondExprOneOrKeyValueNameNotNull($condExprs, $this->keyValueName)) {
return \false;
}
/** @var string $keyValueName */
$keyValueName = $this->keyValueName;
if ($this->countValueName !== null) {
return $this->forAnalyzer->isCondExprSmallerOrGreater($condExprs, $keyValueName, $this->countValueName);
}
if (!$condExprs[0] instanceof BinaryOp) {
return \false;
}
$funcCall = $condExprs[0]->right;
if (!$funcCall instanceof FuncCall) {
return \false;
}
if ($this->nodeNameResolver->isName($funcCall, self::COUNT) && $funcCall->args[0] instanceof Arg) {
$this->iteratedExpr = $funcCall->args[0]->value;
return \true;
}
return \false;
}
}