Skip to content

Commit bb034b9

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

10 files changed

Lines changed: 304 additions & 0 deletions

File tree

config/set/php86.php

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

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)