diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/basic_usage.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/basic_usage.php.inc index b146121786c..e392146c228 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/basic_usage.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/basic_usage.php.inc @@ -68,4 +68,4 @@ class BasicUsage } } -?> \ No newline at end of file +?> diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_basic_usage.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_basic_usage.php.inc new file mode 100644 index 00000000000..ba71a7e1c5d --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_basic_usage.php.inc @@ -0,0 +1,60 @@ + 10) { + return true; + } + } + return false; + } + + public function checkWithKey(array $items) + { + foreach ($items as $key => $value) { + if ($value === 'target') { + return true; + } + } + return false; + } +} +?> +----- + str_starts_with($animal, 'c')); + } + + public function checkNumber(array $numbers) + { + return array_any($numbers, fn($number) => $number > 10); + } + + public function checkWithKey(array $items) + { + return array_any($items, fn($value) => $value === 'target'); + } +} +?> diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_skip_missing_return_false.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_skip_missing_return_false.php.inc new file mode 100644 index 00000000000..9717f2e9b74 --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/early_return_skip_missing_return_false.php.inc @@ -0,0 +1,15 @@ + $animal) { + if (str_starts_with($animal, 'c') && $key === 1) { + return true; + } + } + return false; + } +} + +?> +----- + str_starts_with($animal, 'c') && $key === 1); + } +} + +?> diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_multiple_statements.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_multiple_statements.php.inc index 598b2c446d3..6d5c0f3573c 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_multiple_statements.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_multiple_statements.php.inc @@ -16,4 +16,4 @@ class SkipMultipleStatements } return $found; } -} \ No newline at end of file +} diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_boolean_initialization.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_boolean_initialization.php.inc index 01157b79610..02d1a52c740 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_boolean_initialization.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_boolean_initialization.php.inc @@ -15,4 +15,4 @@ class SkipNoBooleanInitialization } return $found; } -} \ No newline at end of file +} diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_break.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_break.php.inc index 3ea5fbbb2af..8576cf5b892 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_break.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_no_break.php.inc @@ -14,4 +14,4 @@ class SkipNoBreak } return $found; } -} \ No newline at end of file +} diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_variable_reuse_after_foreach.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_variable_reuse_after_foreach.php.inc index d1f0e680ed5..b5f8f12d9fa 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_variable_reuse_after_foreach.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/skip_variable_reuse_after_foreach.php.inc @@ -20,4 +20,4 @@ class SkipVariableReUseAfterForeach return $found; } -} \ No newline at end of file +} diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/with_key.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/with_key.php.inc index 76a2fb1a5e4..2258c079a6e 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/with_key.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAnyRector/Fixture/with_key.php.inc @@ -32,4 +32,4 @@ class WithKey } } -?> \ No newline at end of file +?> diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php index ab34cad777a..3ff1f72a6b9 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAnyRector.php @@ -5,6 +5,7 @@ namespace Rector\Php84\Rector\Foreach_; use PhpParser\Node; +use PhpParser\Node\Expr; use PhpParser\Node\Expr\ArrowFunction; use PhpParser\Node\Expr\Assign; use PhpParser\Node\Expr\Variable; @@ -13,6 +14,7 @@ use PhpParser\Node\Stmt\Expression; use PhpParser\Node\Stmt\Foreach_; use PhpParser\Node\Stmt\If_; +use PhpParser\Node\Stmt\Return_; use Rector\Contract\PhpParser\Node\StmtsAwareInterface; use Rector\NodeManipulator\StmtsManipulator; use Rector\Php84\NodeAnalyzer\ForeachKeyUsedInConditionalAnalyzer; @@ -30,15 +32,15 @@ final class ForeachToArrayAnyRector extends AbstractRector implements MinPhpVers { public function __construct( private readonly ValueResolver $valueResolver, + private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer, private readonly StmtsManipulator $stmtsManipulator, - private readonly ForeachKeyUsedInConditionalAnalyzer $foreachKeyUsedInConditionalAnalyzer ) { } public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Replace foreach with boolean assignment and break with array_any', + 'Replace foreach with boolean assignment + break OR foreach with early return with array_any', [ new CodeSample( <<<'CODE_SAMPLE' @@ -53,6 +55,20 @@ public function getRuleDefinition(): RuleDefinition , <<<'CODE_SAMPLE' $found = array_any($animals, fn($animal) => str_starts_with($animal, 'c')); +CODE_SAMPLE + ), + new CodeSample( + <<<'CODE_SAMPLE' +foreach ($animals as $animal) { + if (str_starts_with($animal, 'c')) { + return true; + } +} +return false; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +return array_any($animals, fn($animal) => str_starts_with($animal, 'c')); CODE_SAMPLE ), ] @@ -76,6 +92,17 @@ public function refactor(Node $node): ?Node return null; } + return $this->refactorBooleanAssignmentPattern($node) + ?? $this->refactorEarlyReturnPattern($node); + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::ARRAY_ANY; + } + + private function refactorBooleanAssignmentPattern(StmtsAwareInterface $node): ?Node + { foreach ($node->stmts as $key => $stmt) { if (! $stmt instanceof Foreach_) { continue; @@ -103,7 +130,7 @@ public function refactor(Node $node): ?Node $assignedVariable = $prevAssign->var; - if (! $this->isValidForeachStructure($foreach, $assignedVariable)) { + if (! $this->isValidBooleanAssignmentForeachStructure($foreach, $assignedVariable)) { continue; } @@ -155,12 +182,67 @@ public function refactor(Node $node): ?Node return null; } - public function provideMinPhpVersion(): int + private function refactorEarlyReturnPattern(StmtsAwareInterface $node): ?Node { - return PhpVersionFeature::ARRAY_ANY; + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Foreach_) { + continue; + } + + $foreach = $stmt; + $nextStmt = $node->stmts[$key + 1] ?? null; + + if (! $nextStmt instanceof Return_) { + continue; + } + + if (! $nextStmt->expr instanceof Expr) { + continue; + } + + if (! $this->valueResolver->isFalse($nextStmt->expr)) { + continue; + } + + if (! $this->isValidEarlyReturnForeachStructure($foreach)) { + continue; + } + + /** @var If_ $firstNodeInsideForeach */ + $firstNodeInsideForeach = $foreach->stmts[0]; + $condition = $firstNodeInsideForeach->cond; + + $params = []; + + if ($foreach->valueVar instanceof Variable) { + $params[] = new Param($foreach->valueVar); + } + + if ( + $foreach->keyVar instanceof Variable && + $this->foreachKeyUsedInConditionalAnalyzer->isUsed($foreach->keyVar, $condition) + ) { + $params[] = new Param(new Variable((string) $this->getName($foreach->keyVar))); + } + + $arrowFunction = new ArrowFunction([ + 'params' => $params, + 'expr' => $condition, + ]); + + $funcCall = $this->nodeFactory->createFuncCall('array_any', [$foreach->expr, $arrowFunction]); + + $node->stmts[$key] = new Return_($funcCall); + unset($node->stmts[$key + 1]); + $node->stmts = array_values($node->stmts); + + return $node; + } + + return null; } - private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool + private function isValidBooleanAssignmentForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool { if (count($foreach->stmts) !== 1) { return false; @@ -199,4 +281,40 @@ private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVa return $type->isArray() ->yes(); } + + private function isValidEarlyReturnForeachStructure(Foreach_ $foreach): bool + { + if (count($foreach->stmts) !== 1) { + return false; + } + + if (! $foreach->stmts[0] instanceof If_) { + return false; + } + + $ifStmt = $foreach->stmts[0]; + + if (count($ifStmt->stmts) !== 1) { + return false; + } + + if (! $ifStmt->stmts[0] instanceof Return_) { + return false; + } + + $returnStmt = $ifStmt->stmts[0]; + + if (! $returnStmt->expr instanceof Expr) { + return false; + } + + if (! $this->valueResolver->isTrue($returnStmt->expr)) { + return false; + } + + $type = $this->nodeTypeResolver->getNativeType($foreach->expr); + + return $type->isArray() + ->yes(); + } }