From a8d2cee0db5dbccfec1e67680fdf44be95a2fe61 Mon Sep 17 00:00:00 2001 From: Orest Divintari Date: Tue, 23 Sep 2025 13:37:06 +0200 Subject: [PATCH 1/3] Move boolean assignment pattern into a method --- .../Rector/Foreach_/ForeachToArrayAllRector.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php index 96629535526..c54aefa833c 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php @@ -77,6 +77,16 @@ public function refactor(Node $node): ?Node return null; } + return $this->refactorBooleanAssignmentPattern($node); + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::ARRAY_ALL; + } + + private function refactorBooleanAssignmentPattern(StmtsAwareInterface $node): ?Node + { foreach ($node->stmts as $key => $stmt) { if (! $stmt instanceof Foreach_) { continue; @@ -158,11 +168,6 @@ public function refactor(Node $node): ?Node return null; } - public function provideMinPhpVersion(): int - { - return PhpVersionFeature::ARRAY_ALL; - } - private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool { if (count($foreach->stmts) !== 1) { From 521364e7b8ca4a4fb07f325eb8496215eeb625e3 Mon Sep 17 00:00:00 2001 From: Orest Divintari Date: Tue, 23 Sep 2025 13:42:15 +0200 Subject: [PATCH 2/3] rename method --- rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php index c54aefa833c..b6c99b6a3e7 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php @@ -114,7 +114,7 @@ private function refactorBooleanAssignmentPattern(StmtsAwareInterface $node): ?N $assignedVariable = $prevAssign->var; - if (! $this->isValidForeachStructure($foreach, $assignedVariable)) { + if (! $this->isValidBooleanAssignmentForeachStructure($foreach, $assignedVariable)) { continue; } @@ -168,7 +168,7 @@ private function refactorBooleanAssignmentPattern(StmtsAwareInterface $node): ?N 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; From b4841915b6b024703a6d5a2c101de5a68aeba2bd Mon Sep 17 00:00:00 2001 From: Orest Divintari Date: Tue, 23 Sep 2025 17:02:50 +0200 Subject: [PATCH 3/3] Update ForeachToArrayAll --- .../Fixture/early_return_basic_usage.php.inc | 62 +++++++++ ...ly_return_skip_missing_return_true.php.inc | 17 +++ ..._multiple_statements_after_foreach.php.inc | 18 +++ ...ple_statements_within_if_statement.php.inc | 18 +++ ...y_return_skip_nested_if_statements.php.inc | 20 +++ .../early_return_skip_non_array.php.inc | 18 +++ ...return_skip_unexpected_return_type.php.inc | 17 +++ .../Fixture/early_return_with_key.php.inc | 32 +++++ .../Fixture/with_key.php.inc | 2 +- .../Foreach_/ForeachToArrayAllRector.php | 118 +++++++++++++++++- 10 files changed, 319 insertions(+), 3 deletions(-) create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_basic_usage.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_missing_return_true.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_after_foreach.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_within_if_statement.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_nested_if_statements.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_non_array.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_unexpected_return_type.php.inc create mode 100644 rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_with_key.php.inc diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_basic_usage.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_basic_usage.php.inc new file mode 100644 index 00000000000..fcc73afcb43 --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_basic_usage.php.inc @@ -0,0 +1,62 @@ + 10)) { + return false; + } + } + return true; + } + + public function checkAllItemsEqualTarget(array $items) + { + foreach ($items as $key => $value) { + if (!($value === 'target')) { + return false; + } + } + return true; + } +} + +?> +----- + str_starts_with($animal, 'c')); + } + + public function checkAllNumbersGreaterThanTen(array $numbers) + { + return array_all($numbers, fn($number) => $number > 10); + } + + public function checkAllItemsEqualTarget(array $items) + { + return array_all($items, fn($value) => $value === 'target'); + } +} + +?> diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_missing_return_true.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_missing_return_true.php.inc new file mode 100644 index 00000000000..b1c4cedff81 --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_missing_return_true.php.inc @@ -0,0 +1,17 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_after_foreach.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_after_foreach.php.inc new file mode 100644 index 00000000000..812362336a3 --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_after_foreach.php.inc @@ -0,0 +1,18 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_within_if_statement.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_within_if_statement.php.inc new file mode 100644 index 00000000000..98bdf075a4b --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_multiple_statements_within_if_statement.php.inc @@ -0,0 +1,18 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_nested_if_statements.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_nested_if_statements.php.inc new file mode 100644 index 00000000000..89a0b47f702 --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_nested_if_statements.php.inc @@ -0,0 +1,20 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_non_array.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_non_array.php.inc new file mode 100644 index 00000000000..0e99c69ffad --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_non_array.php.inc @@ -0,0 +1,18 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_unexpected_return_type.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_unexpected_return_type.php.inc new file mode 100644 index 00000000000..d97e9a2f02e --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_skip_unexpected_return_type.php.inc @@ -0,0 +1,17 @@ + diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_with_key.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_with_key.php.inc new file mode 100644 index 00000000000..225ada3af3f --- /dev/null +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/early_return_with_key.php.inc @@ -0,0 +1,32 @@ + $v) { + if (!isset($params[$k]) || (string) $params[$k] !== (string) $v) { + return false; + } + } + return true; + } +} + +?> +----- + !(!isset($params[$k]) || (string) $params[$k] !== (string) $v)); + } +} + +?> diff --git a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/with_key.php.inc b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/with_key.php.inc index f529df69767..8e0a9fac68b 100644 --- a/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/with_key.php.inc +++ b/rules-tests/Php84/Rector/Foreach_/ForeachToArrayAllRector/Fixture/with_key.php.inc @@ -32,4 +32,4 @@ class WithKey } } -?> \ No newline at end of file +?> diff --git a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php index b6c99b6a3e7..de0c9230f88 100644 --- a/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.php +++ b/rules/Php84/Rector/Foreach_/ForeachToArrayAllRector.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\BooleanNot; @@ -14,6 +15,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; @@ -39,7 +41,7 @@ public function __construct( public function getRuleDefinition(): RuleDefinition { return new RuleDefinition( - 'Replace foreach with boolean assignment and break with array_all', + 'Replace foreach with boolean assignment and break OR foreach with early return with array_all', [ new CodeSample( <<<'CODE_SAMPLE' @@ -54,6 +56,20 @@ public function getRuleDefinition(): RuleDefinition , <<<'CODE_SAMPLE' $found = array_all($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 false; + } +} +return true; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +return array_all($animals, fn($animal) => str_starts_with($animal, 'c')); CODE_SAMPLE ), ] @@ -77,7 +93,8 @@ public function refactor(Node $node): ?Node return null; } - return $this->refactorBooleanAssignmentPattern($node); + return $this->refactorBooleanAssignmentPattern($node) ?? + $this->refactorEarlyReturnPattern($node); } public function provideMinPhpVersion(): int @@ -168,6 +185,103 @@ private function refactorBooleanAssignmentPattern(StmtsAwareInterface $node): ?N return null; } + private function refactorEarlyReturnPattern(StmtsAwareInterface $node): ?Node + { + 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->isTrue($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))); + } + + $negatedCondition = $condition instanceof BooleanNot ? $condition->expr : new BooleanNot($condition); + + $arrowFunction = new ArrowFunction([ + 'params' => $params, + 'expr' => $negatedCondition, + ]); + + $funcCall = $this->nodeFactory->createFuncCall('array_all', [$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 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->isFalse($returnStmt->expr)) { + return false; + } + + $type = $this->nodeTypeResolver->getNativeType($foreach->expr); + + return $type->isArray() + ->yes(); + } + private function isValidBooleanAssignmentForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool { if (count($foreach->stmts) !== 1) {