From 62f22fa94a0f699165c6ac41f3bd679302984fda Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 2 Oct 2025 22:07:04 +0200 Subject: [PATCH] [code-quality] Add RepeatedOrEqualToInArrayRector --- phpstan.neon | 11 ++ .../Fixture/compare_same_variable.php.inc | 35 ++++ .../Fixture/non_strict_in_array.php.inc | 35 ++++ .../Fixture/skip_two_or_less.php.inc | 15 ++ .../RepeatedOrEqualToInArrayRectorTest.php | 28 +++ .../config/configured_rule.php | 9 + .../RepeatedOrEqualToInArrayRector.php | 179 ++++++++++++++++++ .../CompleteMissingIfElseBracketRector.php | 2 +- .../ValueObject/ComparedExprAndValueExpr.php | 26 +++ rules/Php70/EregToPcreTransformer.php | 4 +- src/Config/Level/CodeQualityLevel.php | 2 + 11 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/compare_same_variable.php.inc create mode 100644 rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/non_strict_in_array.php.inc create mode 100644 rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/skip_two_or_less.php.inc create mode 100644 rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/RepeatedOrEqualToInArrayRectorTest.php create mode 100644 rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/config/configured_rule.php create mode 100644 rules/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector.php create mode 100644 rules/CodeQuality/ValueObject/ComparedExprAndValueExpr.php diff --git a/phpstan.neon b/phpstan.neon index 756eba937d9..5316d19e84e 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -345,3 +345,14 @@ parameters: - identifier: symplify.noReference message: '#Use explicit return value over magic &reference#' + + # known type + - + identifier: argument.type + 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#' + + - + path: rules/Renaming/Rector/Name/RenameClassRector.php + identifier: return.type + + - '#Method Rector\\(.*?)Rector\:\:refactor\(\) never returns \d so it can be removed from the return type#' diff --git a/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/compare_same_variable.php.inc b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/compare_same_variable.php.inc new file mode 100644 index 00000000000..4a53a137abf --- /dev/null +++ b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/compare_same_variable.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/non_strict_in_array.php.inc b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/non_strict_in_array.php.inc new file mode 100644 index 00000000000..3dfc1a169cd --- /dev/null +++ b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/non_strict_in_array.php.inc @@ -0,0 +1,35 @@ + +----- + diff --git a/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/skip_two_or_less.php.inc b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/skip_two_or_less.php.inc new file mode 100644 index 00000000000..75309e453da --- /dev/null +++ b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/Fixture/skip_two_or_less.php.inc @@ -0,0 +1,15 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/config/configured_rule.php b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/config/configured_rule.php new file mode 100644 index 00000000000..1d9c0ea9aaa --- /dev/null +++ b/rules-tests/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([RepeatedOrEqualToInArrayRector::class]); diff --git a/rules/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector.php b/rules/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector.php new file mode 100644 index 00000000000..4d822bad2fd --- /dev/null +++ b/rules/CodeQuality/Rector/BooleanOr/RepeatedOrEqualToInArrayRector.php @@ -0,0 +1,179 @@ +> + */ + public function getNodeTypes(): array + { + return [BooleanOr::class]; + } + + /** + * @param BooleanOr $node + */ + public function refactor(Node $node): ?FuncCall + { + $identicals = $this->betterNodeFinder->findInstanceOf($node, Identical::class); + $equals = $this->betterNodeFinder->findInstanceOf($node, Equal::class); + + if (! $this->isEqualOrIdentical($node->right)) { + return null; + } + + // match compared variable and expr + if (! $node->left instanceof BooleanOr && ! $this->isEqualOrIdentical($node->left)) { + return null; + } + + $comparedExprAndValueExprs = $this->matchComparedAndDesiredValues($node); + if ($comparedExprAndValueExprs === null) { + return null; + } + + if (count($comparedExprAndValueExprs) < 3) { + return null; + } + + // ensure all compared expr are the same + $valueExprs = $this->resolveValueExprs($comparedExprAndValueExprs); + + /** @var ComparedExprAndValueExpr $firstComparedExprAndValue */ + $firstComparedExprAndValue = array_pop($comparedExprAndValueExprs); + + // all compared expr must be equal + foreach ($comparedExprAndValueExprs as $comparedExprAndValueExpr) { + if (! $this->nodeComparator->areNodesEqual( + $firstComparedExprAndValue->getComparedExpr(), + $comparedExprAndValueExpr->getComparedExpr() + )) { + return null; + } + } + + $array = $this->nodeFactory->createArray($valueExprs); + + $args = $this->nodeFactory->createArgs([$firstComparedExprAndValue->getComparedExpr(), $array]); + + if ($identicals !== [] && $equals === []) { + $args[] = new Arg(new ConstFetch(new Name('true'))); + } + + return new FuncCall(new Name('in_array'), $args); + } + + private function isEqualOrIdentical(Expr $expr): bool + { + if ($expr instanceof Identical) { + return true; + } + + return $expr instanceof Equal; + } + + private function matchComparedExprAndValueExpr(Identical|Equal $expr): ComparedExprAndValueExpr + { + return new ComparedExprAndValueExpr($expr->left, $expr->right); + } + + /** + * @param ComparedExprAndValueExpr[] $comparedExprAndValueExprs + * @return Expr[] + */ + private function resolveValueExprs(array $comparedExprAndValueExprs): array + { + $valueExprs = []; + + foreach ($comparedExprAndValueExprs as $comparedExprAndValueExpr) { + $valueExprs[] = $comparedExprAndValueExpr->getValueExpr(); + } + + return $valueExprs; + } + + /** + * @return null|ComparedExprAndValueExpr[] + */ + private function matchComparedAndDesiredValues(BooleanOr $booleanOr): ?array + { + /** @var Identical|Equal $rightCompare */ + $rightCompare = $booleanOr->right; + + // match compared expr and desired value + $comparedExprAndValueExprs = [$this->matchComparedExprAndValueExpr($rightCompare)]; + + $currentBooleanOr = $booleanOr; + + while ($currentBooleanOr->left instanceof BooleanOr) { + if (! $this->isEqualOrIdentical($currentBooleanOr->left->right)) { + return null; + } + + $comparedExprAndValueExprs[] = $this->matchComparedExprAndValueExpr($currentBooleanOr->left->right); + $currentBooleanOr = $currentBooleanOr->left; + } + + if (! $this->isEqualOrIdentical($currentBooleanOr->left)) { + return null; + } + + /** @var Identical|Equal $leftCompare */ + $leftCompare = $currentBooleanOr->left; + + $comparedExprAndValueExprs[] = $this->matchComparedExprAndValueExpr($leftCompare); + + // keep original natural order, as left/right goes from bottom up + return array_reverse($comparedExprAndValueExprs); + } +} diff --git a/rules/CodeQuality/Rector/If_/CompleteMissingIfElseBracketRector.php b/rules/CodeQuality/Rector/If_/CompleteMissingIfElseBracketRector.php index e7c8d4776a3..1b950cc5ebb 100644 --- a/rules/CodeQuality/Rector/If_/CompleteMissingIfElseBracketRector.php +++ b/rules/CodeQuality/Rector/If_/CompleteMissingIfElseBracketRector.php @@ -4,12 +4,12 @@ namespace Rector\CodeQuality\Rector\If_; -use PhpParser\Token; use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Else_; use PhpParser\Node\Stmt\ElseIf_; use PhpParser\Node\Stmt\If_; +use PhpParser\Token; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; diff --git a/rules/CodeQuality/ValueObject/ComparedExprAndValueExpr.php b/rules/CodeQuality/ValueObject/ComparedExprAndValueExpr.php new file mode 100644 index 00000000000..50ef45ae862 --- /dev/null +++ b/rules/CodeQuality/ValueObject/ComparedExprAndValueExpr.php @@ -0,0 +1,26 @@ +comparedExpr; + } + + public function getValueExpr(): Expr + { + return $this->valueExpr; + } +} diff --git a/rules/Php70/EregToPcreTransformer.php b/rules/Php70/EregToPcreTransformer.php index 6b2a48deaa6..4c1c68aadad 100644 --- a/rules/Php70/EregToPcreTransformer.php +++ b/rules/Php70/EregToPcreTransformer.php @@ -136,7 +136,7 @@ private function _ere2pcre(string $content, int $i): array $r[$rr] .= '[' . $cls . ']'; } elseif ($char === ')') { break; - } elseif ($char === '*' || $char === '+' || $char === '?') { + } elseif (in_array($char, ['*', '+', '?'], true)) { throw new InvalidEregException('unescaped metacharacter "' . $char . '"'); } elseif ($char === '{') { if ($i + 1 < $l && \str_contains('0123456789', $content[$i + 1])) { @@ -177,7 +177,7 @@ private function _ere2pcre(string $content, int $i): array // piece after the atom (only ONE of them is possible) $char = $content[$i]; - if ($char === '*' || $char === '+' || $char === '?') { + if (in_array($char, ['*', '+', '?'], true)) { $r[$rr] .= $char; ++$i; } elseif ($char === '{') { diff --git a/src/Config/Level/CodeQualityLevel.php b/src/Config/Level/CodeQualityLevel.php index 166f12a5607..de46a440cc4 100644 --- a/src/Config/Level/CodeQualityLevel.php +++ b/src/Config/Level/CodeQualityLevel.php @@ -9,6 +9,7 @@ use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector; use Rector\CodeQuality\Rector\BooleanNot\ReplaceMultipleBooleanNotRector; use Rector\CodeQuality\Rector\BooleanNot\SimplifyDeMorganBinaryRector; +use Rector\CodeQuality\Rector\BooleanOr\RepeatedOrEqualToInArrayRector; use Rector\CodeQuality\Rector\Catch_\ThrowWithPreviousExceptionRector; use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector; use Rector\CodeQuality\Rector\Class_\ConvertStaticToSelfRector; @@ -104,6 +105,7 @@ final class CodeQualityLevel SimplifyEmptyArrayCheckRector::class, ReplaceMultipleBooleanNotRector::class, ForeachToInArrayRector::class, + RepeatedOrEqualToInArrayRector::class, SimplifyForeachToCoalescingRector::class, SimplifyFuncGetArgsCountRector::class, SimplifyInArrayValuesRector::class,