Skip to content

Commit 998b600

Browse files
committed
feat: add min/max to clamp rector
1 parent 98f0cb1 commit 998b600

File tree

11 files changed

+300
-2
lines changed

11 files changed

+300
-2
lines changed

config/set/php86.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
declare(strict_types=1);
44

55
use Rector\Config\RectorConfig;
6+
use Rector\Php86\Rector\FuncCall\MinMaxToClampRector;
67

78
return static function (RectorConfig $rectorConfig): void {
8-
$rectorConfig->rules([
9-
]);
9+
$rectorConfig->rules([MinMaxToClampRector::class]);
1010
};
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\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
4+
5+
$value = random_int(0, 100);
6+
7+
max(0, min(100, $value));
8+
max(0, min($value, 100));
9+
max(min($value, 100), 0);
10+
max(min(100, $value), 0);
11+
12+
min(100, max(0, $value));
13+
min(100, max($value, 0));
14+
min(max($value, 0), 100);
15+
min(max(0, $value), 100);
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
22+
23+
$value = random_int(0, 100);
24+
25+
clamp($value, 0, 100);
26+
clamp($value, 0, 100);
27+
clamp($value, 0, 100);
28+
clamp($value, 0, 100);
29+
30+
clamp($value, 0, 100);
31+
clamp($value, 0, 100);
32+
clamp($value, 0, 100);
33+
clamp($value, 0, 100);
34+
35+
?>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
4+
5+
$limit = 100;
6+
7+
fn (int $value) => max(0, min(100, $value));
8+
fn (int $value) => max(0, min($limit, $value));
9+
fn (int $value) => max(0, min($value, $limit));
10+
fn (int $value) => max(PHP_INT_MIN, min(PHP_INT_MAX, $value));
11+
fn (string $value) => max('a', min('z', $value));
12+
13+
?>
14+
-----
15+
<?php
16+
17+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
18+
19+
$limit = 100;
20+
21+
fn (int $value) => clamp($value, 0, 100);
22+
fn (int $value) => clamp($value, 0, $limit);
23+
fn (int $value) => clamp($value, 0, $limit);
24+
fn (int $value) => clamp($value, PHP_INT_MIN, PHP_INT_MAX);
25+
fn (string $value) => clamp($value, 'a', 'z');
26+
27+
?>
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\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
4+
5+
fn (int $value, int $min) => max($min, min(100, $value));
6+
fn (int $value, int $max) => min($max, max(0, $value));
7+
8+
?>
9+
-----
10+
<?php
11+
12+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
13+
14+
fn (int $value, int $min) => clamp($value, $min, 100);
15+
fn (int $value, int $max) => clamp($value, 0, $max);
16+
17+
?>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
4+
5+
fn (int $value, int $min, int $max) => max($min, min($value, $max));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\Fixture;
4+
5+
fn (int $value, int $min, int $max) => max($min, max($max, $value));
6+
fn (int $value, int $min, int $max) => min($min, min($max, $value));
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\Php86\Rector\FuncCall\MinMaxToClampRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class MinMaxToClampRectorTest 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: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Php86\Rector\FuncCall\MinMaxToClampRector;
7+
use Rector\ValueObject\PhpVersion;
8+
9+
return static function (RectorConfig $rectorConfig): void {
10+
$rectorConfig->rule(MinMaxToClampRector::class);
11+
12+
$rectorConfig->phpVersion(PhpVersion::PHP_86);
13+
};
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Php86\Rector\FuncCall;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr;
10+
use PhpParser\Node\Expr\FuncCall;
11+
use Rector\Rector\AbstractRector;
12+
use Rector\ValueObject\PhpVersionFeature;
13+
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
14+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
15+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
16+
17+
/**
18+
* @see \Rector\Tests\Php86\Rector\FuncCall\MinMaxToClampRector\MinMaxToClampRectorTest
19+
*/
20+
final class MinMaxToClampRector extends AbstractRector implements MinPhpVersionInterface
21+
{
22+
public function getRuleDefinition(): RuleDefinition
23+
{
24+
return new RuleDefinition('Convert nested min()/max() calls to clamp()', [
25+
new CodeSample(
26+
<<<'CODE_SAMPLE'
27+
$result = max(0, min(100, $value));
28+
CODE_SAMPLE
29+
,
30+
<<<'CODE_SAMPLE'
31+
$result = clamp($value, 0, 100);
32+
CODE_SAMPLE
33+
),
34+
]);
35+
}
36+
37+
public function getNodeTypes(): array
38+
{
39+
return [FuncCall::class];
40+
}
41+
42+
/**
43+
* @param FuncCall $node
44+
*/
45+
public function refactor(Node $node): ?Node
46+
{
47+
if ($node->isFirstClassCallable()) {
48+
return null;
49+
}
50+
51+
if ($this->isName($node, 'max')) {
52+
return $this->matchClampFuncCall($node, 'min');
53+
}
54+
55+
if ($this->isName($node, 'min')) {
56+
return $this->matchClampFuncCall($node, 'max');
57+
}
58+
59+
return null;
60+
}
61+
62+
public function provideMinPhpVersion(): int
63+
{
64+
return PhpVersionFeature::CLAMP;
65+
}
66+
67+
private function matchClampFuncCall(FuncCall $outerFuncCall, string $expectedInnerFuncName): ?FuncCall
68+
{
69+
$args = $outerFuncCall->getArgs();
70+
71+
if (count($args) !== 2) {
72+
return null;
73+
}
74+
75+
if (! $this->isSupportedArg($args[0]) || ! $this->isSupportedArg($args[1])) {
76+
return null;
77+
}
78+
79+
$leftValue = $args[0]->value;
80+
$rightValue = $args[1]->value;
81+
82+
if ($leftValue instanceof FuncCall) {
83+
return $this->createClampFuncCall($outerFuncCall, $leftValue, $rightValue, $expectedInnerFuncName);
84+
}
85+
86+
if ($rightValue instanceof FuncCall) {
87+
return $this->createClampFuncCall($outerFuncCall, $rightValue, $leftValue, $expectedInnerFuncName);
88+
}
89+
90+
return null;
91+
}
92+
93+
private function createClampFuncCall(
94+
FuncCall $outerFuncCall,
95+
FuncCall $innerFuncCall,
96+
Expr $outerBoundExpr,
97+
string $expectedInnerFuncName
98+
): ?FuncCall {
99+
if ($innerFuncCall->isFirstClassCallable()) {
100+
return null;
101+
}
102+
103+
if (! $this->isName($innerFuncCall, $expectedInnerFuncName)) {
104+
return null;
105+
}
106+
107+
$args = $innerFuncCall->getArgs();
108+
if (count($args) !== 2) {
109+
return null;
110+
}
111+
112+
if (! $this->isSupportedArg($args[0]) || ! $this->isSupportedArg($args[1])) {
113+
return null;
114+
}
115+
116+
$valueAndBound = $this->matchValueAndKnownBound($args[0]->value, $args[1]->value);
117+
if ($valueAndBound === null) {
118+
return null;
119+
}
120+
121+
[$valueExpr, $innerBoundExpr] = $valueAndBound;
122+
123+
if ($this->isName($outerFuncCall, 'max')) {
124+
return $this->nodeFactory->createFuncCall('clamp', [$valueExpr, $outerBoundExpr, $innerBoundExpr]);
125+
}
126+
127+
return $this->nodeFactory->createFuncCall('clamp', [$valueExpr, $innerBoundExpr, $outerBoundExpr]);
128+
}
129+
130+
private function isSupportedArg(Arg $arg): bool
131+
{
132+
return ! $arg->unpack && $arg->name === null;
133+
}
134+
135+
/**
136+
* @return array{Expr, Expr}|null
137+
*/
138+
private function matchValueAndKnownBound(Expr $firstExpr, Expr $secondExpr): ?array
139+
{
140+
$isFirstKnownBound = $this->isKnownBound($firstExpr);
141+
$isSecondKnownBound = $this->isKnownBound($secondExpr);
142+
143+
if ($isFirstKnownBound === $isSecondKnownBound) {
144+
return null;
145+
}
146+
147+
if ($isFirstKnownBound) {
148+
return [$secondExpr, $firstExpr];
149+
}
150+
151+
return [$firstExpr, $secondExpr];
152+
}
153+
154+
private function isKnownBound(Expr $expr): bool
155+
{
156+
return $this->getType($expr)
157+
->isConstantScalarValue()
158+
->yes();
159+
}
160+
}

src/Config/Level/CodingStyleLevel.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Rector\CodingStyle\Rector\Use_\SeparateMultiUseImportsRector;
3333
use Rector\Contract\Rector\RectorInterface;
3434
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
35+
use Rector\Php86\Rector\FuncCall\MinMaxToClampRector;
3536
use Rector\Transform\Rector\FuncCall\FuncCallToConstFetchRector;
3637
use Rector\Visibility\Rector\ClassMethod\ExplicitPublicClassMethodRector;
3738

@@ -83,6 +84,7 @@ final class CodingStyleLevel
8384
ExplicitPublicClassMethodRector::class,
8485
RemoveUselessAliasInUseStatementRector::class,
8586
BinaryOpStandaloneAssignsToDirectRector::class,
87+
MinMaxToClampRector::class,
8688
];
8789

8890
/**

0 commit comments

Comments
 (0)