Skip to content

Commit 9da5fab

Browse files
committed
feat: add min/max to clamp rector
1 parent 31e72a6 commit 9da5fab

File tree

10 files changed

+304
-2
lines changed

10 files changed

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

src/ValueObject/PhpVersionFeature.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,4 +500,9 @@ final class PhpVersionFeature
500500
* @see https://wiki.php.net/rfc/override_properties
501501
*/
502502
public const int OVERRIDE_ATTRIBUTE_ON_PROPERTIES = PhpVersion::PHP_85;
503+
504+
/**
505+
* @see https://wiki.php.net/rfc/clamp_v2
506+
*/
507+
public const int CLAMP = PhpVersion::PHP_86;
503508
}

0 commit comments

Comments
 (0)