Skip to content

Commit dc677e1

Browse files
authored
Implement array_any upgrade (#7010)
1 parent d5a9954 commit dc677e1

File tree

9 files changed

+369
-0
lines changed

9 files changed

+369
-0
lines changed

config/set/php84.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Rector\Php84\Rector\Foreach_\ForeachToArrayFindRector;
88
use Rector\Php84\Rector\Foreach_\ForeachToArrayFindKeyRector;
99
use Rector\Php84\Rector\Foreach_\ForeachToArrayAllRector;
10+
use Rector\Php84\Rector\Foreach_\ForeachToArrayAnyRector;
1011
use Rector\Php84\Rector\FuncCall\AddEscapeArgumentRector;
1112
use Rector\Php84\Rector\FuncCall\RoundingModeEnumRector;
1213
use Rector\Php84\Rector\MethodCall\NewMethodCallWithoutParenthesesRector;
@@ -23,6 +24,7 @@
2324
ForeachToArrayFindRector::class,
2425
ForeachToArrayFindKeyRector::class,
2526
ForeachToArrayAllRector::class,
27+
ForeachToArrayAnyRector::class,
2628
]
2729
);
2830
};
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\Fixture;
4+
5+
class BasicUsage
6+
{
7+
public function checkAnimal($animals)
8+
{
9+
$found = false;
10+
foreach ($animals as $animal) {
11+
if (str_starts_with($animal, 'c')) {
12+
$found = true;
13+
break;
14+
}
15+
}
16+
return $found;
17+
}
18+
19+
public function checkNumber($numbers)
20+
{
21+
$exists = false;
22+
foreach ($numbers as $number) {
23+
if ($number > 10) {
24+
$exists = true;
25+
break;
26+
}
27+
}
28+
return $exists;
29+
}
30+
31+
public function checkWithKey($items)
32+
{
33+
$hasMatch = false;
34+
foreach ($items as $key => $value) {
35+
if ($value === 'target') {
36+
$hasMatch = true;
37+
break;
38+
}
39+
}
40+
return $hasMatch;
41+
}
42+
}
43+
44+
?>
45+
-----
46+
<?php
47+
48+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\Fixture;
49+
50+
class BasicUsage
51+
{
52+
public function checkAnimal($animals)
53+
{
54+
$found = array_any($animals, fn($animal) => str_starts_with($animal, 'c'));
55+
return $found;
56+
}
57+
58+
public function checkNumber($numbers)
59+
{
60+
$exists = array_any($numbers, fn($number) => $number > 10);
61+
return $exists;
62+
}
63+
64+
public function checkWithKey($items)
65+
{
66+
$hasMatch = array_any($items, fn($value) => $value === 'target');
67+
return $hasMatch;
68+
}
69+
}
70+
71+
?>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\Fixture;
4+
5+
class SkipMultipleStatements
6+
{
7+
public function run($animals)
8+
{
9+
$found = false;
10+
foreach ($animals as $animal) {
11+
if (str_starts_with($animal, 'c')) {
12+
$found = true;
13+
echo 'Found: ' . $animal;
14+
break;
15+
}
16+
}
17+
return $found;
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\Fixture;
4+
5+
class SkipNoBooleanInitialization
6+
{
7+
public function run($animals)
8+
{
9+
$found = null;
10+
foreach ($animals as $animal) {
11+
if (str_starts_with($animal, 'c')) {
12+
$found = true;
13+
break;
14+
}
15+
}
16+
return $found;
17+
}
18+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\Fixture;
4+
5+
class SkipNoBreak
6+
{
7+
public function run($animals)
8+
{
9+
$found = false;
10+
foreach ($animals as $animal) {
11+
if (str_starts_with($animal, 'c')) {
12+
$found = true;
13+
}
14+
}
15+
return $found;
16+
}
17+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ForeachToArrayAnyRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Php84\Rector\Foreach_\ForeachToArrayAnyRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(ForeachToArrayAnyRector::class);
10+
};
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Php84\Rector\Foreach_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\ArrowFunction;
10+
use PhpParser\Node\Expr\Assign;
11+
use PhpParser\Node\Expr\ConstFetch;
12+
use PhpParser\Node\Expr\Variable;
13+
use PhpParser\Node\Param;
14+
use PhpParser\Node\Stmt\Break_;
15+
use PhpParser\Node\Stmt\Expression;
16+
use PhpParser\Node\Stmt\Foreach_;
17+
use PhpParser\Node\Stmt\If_;
18+
use Rector\Contract\PhpParser\Node\StmtsAwareInterface;
19+
use Rector\Rector\AbstractRector;
20+
use Rector\ValueObject\PhpVersionFeature;
21+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
22+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
23+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
24+
25+
/**
26+
* @see \Rector\Tests\Php84\Rector\Foreach_\ForeachToArrayAnyRector\ForeachToArrayAnyRectorTest
27+
*/
28+
final class ForeachToArrayAnyRector extends AbstractRector implements MinPhpVersionInterface
29+
{
30+
public function getRuleDefinition(): RuleDefinition
31+
{
32+
return new RuleDefinition(
33+
'Replace foreach with boolean assignment and break with array_any',
34+
[
35+
new CodeSample(
36+
<<<'CODE_SAMPLE'
37+
$found = false;
38+
foreach ($animals as $animal) {
39+
if (str_starts_with($animal, 'c')) {
40+
$found = true;
41+
break;
42+
}
43+
}
44+
CODE_SAMPLE
45+
,
46+
<<<'CODE_SAMPLE'
47+
$found = array_any($animals, fn($animal) => str_starts_with($animal, 'c'));
48+
CODE_SAMPLE
49+
),
50+
]
51+
);
52+
}
53+
54+
/**
55+
* @return array<class-string<Node>>
56+
*/
57+
public function getNodeTypes(): array
58+
{
59+
return [StmtsAwareInterface::class];
60+
}
61+
62+
/**
63+
* @param StmtsAwareInterface $node
64+
*/
65+
public function refactor(Node $node): ?Node
66+
{
67+
if ($node->stmts === null) {
68+
return null;
69+
}
70+
71+
foreach ($node->stmts as $key => $stmt) {
72+
if (! $stmt instanceof Foreach_) {
73+
continue;
74+
}
75+
76+
$prevStmt = $node->stmts[$key - 1] ?? null;
77+
if (! $prevStmt instanceof Expression) {
78+
continue;
79+
}
80+
81+
if (! $prevStmt->expr instanceof Assign) {
82+
continue;
83+
}
84+
85+
$foreach = $stmt;
86+
$prevAssign = $prevStmt->expr;
87+
88+
if (! $this->isFalse($prevAssign->expr)) {
89+
continue;
90+
}
91+
92+
if (! $prevAssign->var instanceof Variable) {
93+
continue;
94+
}
95+
96+
$assignedVariable = $prevAssign->var;
97+
98+
if (! $this->isValidForeachStructure($foreach, $assignedVariable)) {
99+
continue;
100+
}
101+
102+
/** @var If_ $firstNodeInsideForeach */
103+
$firstNodeInsideForeach = $foreach->stmts[0];
104+
105+
/** @var Expression $assignmentStmt */
106+
$assignmentStmt = $firstNodeInsideForeach->stmts[0];
107+
/** @var Assign $assignment */
108+
$assignment = $assignmentStmt->expr;
109+
110+
/** @var Break_ $breakStmt */
111+
$breakStmt = $firstNodeInsideForeach->stmts[1];
112+
113+
$condition = $firstNodeInsideForeach->cond;
114+
$valueParam = $foreach->valueVar;
115+
116+
if (! $valueParam instanceof Variable) {
117+
continue;
118+
}
119+
$param = new Param($valueParam);
120+
121+
$arrowFunction = new ArrowFunction([
122+
'params' => [$param],
123+
'expr' => $condition,
124+
]);
125+
126+
$funcCall = $this->nodeFactory->createFuncCall('array_any', [$foreach->expr, $arrowFunction]);
127+
128+
$newAssign = new Assign($assignedVariable, $funcCall);
129+
$newExpression = new Expression($newAssign);
130+
131+
unset($node->stmts[$key - 1]);
132+
$node->stmts[$key] = $newExpression;
133+
134+
$node->stmts = array_values($node->stmts);
135+
136+
return $node;
137+
}
138+
139+
return null;
140+
}
141+
142+
public function provideMinPhpVersion(): int
143+
{
144+
return PhpVersionFeature::ARRAY_ANY;
145+
}
146+
147+
private function isValidForeachStructure(Foreach_ $foreach, Variable $assignedVariable): bool
148+
{
149+
if (count($foreach->stmts) !== 1) {
150+
return false;
151+
}
152+
153+
$firstStmt = $foreach->stmts[0];
154+
if (
155+
! $firstStmt instanceof If_ ||
156+
count($firstStmt->stmts) !== 2
157+
) {
158+
return false;
159+
}
160+
161+
$assignmentStmt = $firstStmt->stmts[0];
162+
$breakStmt = $firstStmt->stmts[1];
163+
164+
if (
165+
! $assignmentStmt instanceof Expression ||
166+
! $assignmentStmt->expr instanceof Assign ||
167+
! $breakStmt instanceof Break_
168+
) {
169+
return false;
170+
}
171+
172+
$assignment = $assignmentStmt->expr;
173+
174+
if (! $this->nodeComparator->areNodesEqual($assignment->var, $assignedVariable)) {
175+
return false;
176+
}
177+
178+
return $this->isTrue($assignment->expr);
179+
}
180+
181+
private function isFalse(Expr $expr): bool
182+
{
183+
if (! $expr instanceof ConstFetch) {
184+
return false;
185+
}
186+
187+
return $this->isName($expr->name, 'false');
188+
}
189+
190+
private function isTrue(Expr $expr): bool
191+
{
192+
if (! $expr instanceof ConstFetch) {
193+
return false;
194+
}
195+
196+
return $this->isName($expr->name, 'true');
197+
}
198+
}

src/ValueObject/PhpVersionFeature.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,4 +756,10 @@ final class PhpVersionFeature
756756
* @var int
757757
*/
758758
public const ARRAY_ALL = PhpVersion::PHP_84;
759+
760+
/**
761+
* @see https://php.watch/versions/8.4/array_find-array_find_key-array_any-array_all
762+
* @var int
763+
*/
764+
public const ARRAY_ANY = PhpVersion::PHP_84;
759765
}

0 commit comments

Comments
 (0)