Skip to content

Commit d2cec5b

Browse files
committed
[code-quality] Add RepeatedOrEqualToInArrayRector
1 parent 9a76de9 commit d2cec5b

File tree

11 files changed

+337
-3
lines changed

11 files changed

+337
-3
lines changed

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,8 @@ parameters:
345345
-
346346
identifier: symplify.noReference
347347
message: '#Use explicit return value over magic &reference#'
348+
349+
# known type
350+
-
351+
identifier: argument.type
352+
message: '#Parameter \#1 \$expr of method Rector\\CodeQuality\\Rector\\BooleanOr\\RepeatedOrEqualToInArrayRector\:\:matchComparedExprAndValueExpr\(\) expects PhpParser\\Node\\Expr\\BinaryOp\\Equal\|PhpParser\\Node\\Expr\\BinaryOp\\Identical, PhpParser\\Node\\Expr given#'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\Fixture;
4+
5+
final class CompareSameVariable
6+
{
7+
public function demo($someVariable): bool
8+
{
9+
if ($someVariable === 'a' || $someVariable === 'b' || $someVariable === 'c') {
10+
return true;
11+
}
12+
13+
return false;
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\Fixture;
22+
23+
final class CompareSameVariable
24+
{
25+
public function demo($someVariable): bool
26+
{
27+
if (in_array($someVariable, ['a', 'b', 'c'], true)) {
28+
return true;
29+
}
30+
31+
return false;
32+
}
33+
}
34+
35+
?>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\Fixture;
4+
5+
final class NonStrictInArray
6+
{
7+
public function demo($someVariable): bool
8+
{
9+
if ($someVariable == 'a' || $someVariable == 'b' || $someVariable == 'c') {
10+
return true;
11+
}
12+
13+
return false;
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\Fixture;
22+
23+
final class NonStrictInArray
24+
{
25+
public function demo($someVariable): bool
26+
{
27+
if (in_array($someVariable, ['a', 'b', 'c'])) {
28+
return true;
29+
}
30+
31+
return false;
32+
}
33+
}
34+
35+
?>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\Fixture;
4+
5+
final class SkipTwoOrLess
6+
{
7+
public function demo($someVariable): bool
8+
{
9+
if ($someVariable === 'a' || $someVariable === 'b') {
10+
return true;
11+
}
12+
13+
return false;
14+
}
15+
}
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\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class RepeatedOrEqualToInArrayRectorTest 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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector;
6+
use Rector\Config\RectorConfig;
7+
8+
return RectorConfig::configure()
9+
->withRules([RepeatedOrEqualToInArrayRector::class]);
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\CodeQuality\Rector\BooleanOr;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr;
10+
use PhpParser\Node\Expr\BinaryOp\BooleanOr;
11+
use PhpParser\Node\Expr\BinaryOp\Equal;
12+
use PhpParser\Node\Expr\BinaryOp\Identical;
13+
use PhpParser\Node\Expr\ConstFetch;
14+
use PhpParser\Node\Expr\FuncCall;
15+
use PhpParser\Node\Name;
16+
use Rector\CodeQuality\ValueObject\ComparedExprAndValueExpr;
17+
use Rector\PhpParser\Node\BetterNodeFinder;
18+
use Rector\Rector\AbstractRector;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
20+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
21+
22+
/**
23+
* @see \Rector\Tests\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector\RepeatedOrEqualToInArrayRectorTest
24+
*/
25+
final class RepeatedOrEqualToInArrayRector extends AbstractRector
26+
{
27+
public function __construct(
28+
private readonly BetterNodeFinder $betterNodeFinder,
29+
) {
30+
}
31+
32+
public function getRuleDefinition(): RuleDefinition
33+
{
34+
return new RuleDefinition(
35+
'Simplify repeated || compare of same value, to in_array() class',
36+
[
37+
new CodeSample(
38+
<<<'CODE_SAMPLE'
39+
if ($value === 10 || $value === 20 || $value === 30) {
40+
// ...
41+
}
42+
43+
CODE_SAMPLE
44+
,
45+
<<<'CODE_SAMPLE'
46+
if (in_array($value, [10, 20, 30], true)) {
47+
// ...
48+
}
49+
CODE_SAMPLE
50+
),
51+
]
52+
);
53+
}
54+
55+
/**
56+
* @return array<class-string<Node>>
57+
*/
58+
public function getNodeTypes(): array
59+
{
60+
return [BooleanOr::class];
61+
}
62+
63+
/**
64+
* @param BooleanOr $node
65+
*/
66+
public function refactor(Node $node): ?FuncCall
67+
{
68+
$identicals = $this->betterNodeFinder->findInstanceOf($node, Identical::class);
69+
$equals = $this->betterNodeFinder->findInstanceOf($node, Equal::class);
70+
71+
if (! $this->isEqualOrIdentical($node->right)) {
72+
return null;
73+
}
74+
75+
// match compared variable and expr
76+
if (! $node->left instanceof BooleanOr && ! $this->isEqualOrIdentical($node->left)) {
77+
return null;
78+
}
79+
80+
$comparedExprAndValueExprs = $this->matchComparedAndDesiredValues($node);
81+
if ($comparedExprAndValueExprs === null) {
82+
return null;
83+
}
84+
85+
if (count($comparedExprAndValueExprs) < 3) {
86+
return null;
87+
}
88+
89+
// ensure all compared expr are the same
90+
$valueExprs = $this->resolveValueExprs($comparedExprAndValueExprs);
91+
92+
/** @var ComparedExprAndValueExpr $firstComparedExprAndValue */
93+
$firstComparedExprAndValue = array_pop($comparedExprAndValueExprs);
94+
95+
// all compared expr must be equal
96+
foreach ($comparedExprAndValueExprs as $comparedExprAndValueExpr) {
97+
if (! $this->nodeComparator->areNodesEqual(
98+
$firstComparedExprAndValue->getComparedExpr(),
99+
$comparedExprAndValueExpr->getComparedExpr()
100+
)) {
101+
return null;
102+
}
103+
}
104+
105+
$array = $this->nodeFactory->createArray($valueExprs);
106+
107+
$args = $this->nodeFactory->createArgs([$firstComparedExprAndValue->getComparedExpr(), $array]);
108+
109+
if ($identicals !== [] && $equals === []) {
110+
$args[] = new Arg(new ConstFetch(new Name('true')));
111+
}
112+
113+
return new FuncCall(new Name('in_array'), $args);
114+
}
115+
116+
private function isEqualOrIdentical(Expr $expr): bool
117+
{
118+
if ($expr instanceof Identical) {
119+
return true;
120+
}
121+
122+
return $expr instanceof Equal;
123+
}
124+
125+
private function matchComparedExprAndValueExpr(Identical|Equal $expr): ComparedExprAndValueExpr
126+
{
127+
return new ComparedExprAndValueExpr($expr->left, $expr->right);
128+
}
129+
130+
/**
131+
* @param ComparedExprAndValueExpr[] $comparedExprAndValueExprs
132+
* @return Expr[]
133+
*/
134+
private function resolveValueExprs(array $comparedExprAndValueExprs): array
135+
{
136+
$valueExprs = [];
137+
138+
foreach ($comparedExprAndValueExprs as $comparedExprAndValueExpr) {
139+
$valueExprs[] = $comparedExprAndValueExpr->getValueExpr();
140+
}
141+
142+
return $valueExprs;
143+
}
144+
145+
/**
146+
* @return null|ComparedExprAndValueExpr[]
147+
*/
148+
private function matchComparedAndDesiredValues(BooleanOr $booleanOr): ?array
149+
{
150+
/** @var Identical|Equal $rightCompare */
151+
$rightCompare = $booleanOr->right;
152+
153+
// match compared expr and desired value
154+
$comparedExprAndValueExprs = [$this->matchComparedExprAndValueExpr($rightCompare)];
155+
156+
$currentBooleanOr = $booleanOr;
157+
158+
while ($currentBooleanOr->left instanceof BooleanOr) {
159+
if (! $this->isEqualOrIdentical($currentBooleanOr->left->right)) {
160+
return null;
161+
}
162+
163+
$comparedExprAndValueExprs[] = $this->matchComparedExprAndValueExpr($currentBooleanOr->left->right);
164+
$currentBooleanOr = $currentBooleanOr->left;
165+
}
166+
167+
if (! $this->isEqualOrIdentical($currentBooleanOr->left)) {
168+
return null;
169+
}
170+
171+
/** @var Identical|Equal $leftCompare */
172+
$leftCompare = $currentBooleanOr->left;
173+
174+
$comparedExprAndValueExprs[] = $this->matchComparedExprAndValueExpr($leftCompare);
175+
176+
// keep original natural order, as left/right goes from bottom up
177+
return array_reverse($comparedExprAndValueExprs);
178+
}
179+
}

rules/CodeQuality/Rector/If_/CompleteMissingIfElseBracketRector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
namespace Rector\CodeQuality\Rector\If_;
66

7-
use PhpParser\Token;
87
use PhpParser\Node;
98
use PhpParser\Node\Stmt;
109
use PhpParser\Node\Stmt\Else_;
1110
use PhpParser\Node\Stmt\ElseIf_;
1211
use PhpParser\Node\Stmt\If_;
12+
use PhpParser\Token;
1313
use Rector\NodeTypeResolver\Node\AttributeKey;
1414
use Rector\Rector\AbstractRector;
1515
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\CodeQuality\ValueObject;
6+
7+
use PhpParser\Node\Expr;
8+
9+
final readonly class ComparedExprAndValueExpr
10+
{
11+
public function __construct(
12+
private Expr $comparedExpr,
13+
private Expr $valueExpr
14+
) {
15+
}
16+
17+
public function getComparedExpr(): Expr
18+
{
19+
return $this->comparedExpr;
20+
}
21+
22+
public function getValueExpr(): Expr
23+
{
24+
return $this->valueExpr;
25+
}
26+
}

rules/Php70/EregToPcreTransformer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ private function _ere2pcre(string $content, int $i): array
136136
$r[$rr] .= '[' . $cls . ']';
137137
} elseif ($char === ')') {
138138
break;
139-
} elseif ($char === '*' || $char === '+' || $char === '?') {
139+
} elseif (in_array($char, ['*', '+', '?'], true)) {
140140
throw new InvalidEregException('unescaped metacharacter "' . $char . '"');
141141
} elseif ($char === '{') {
142142
if ($i + 1 < $l && \str_contains('0123456789', $content[$i + 1])) {
@@ -177,7 +177,7 @@ private function _ere2pcre(string $content, int $i): array
177177

178178
// piece after the atom (only ONE of them is possible)
179179
$char = $content[$i];
180-
if ($char === '*' || $char === '+' || $char === '?') {
180+
if (in_array($char, ['*', '+', '?'], true)) {
181181
$r[$rr] .= $char;
182182
++$i;
183183
} elseif ($char === '{') {

0 commit comments

Comments
 (0)