Skip to content

Commit 2910a2a

Browse files
authored
[DowngradePhp83] Add DowngradeReadonlyAnonymousClassRector (#279)
1 parent f6a888c commit 2910a2a

File tree

10 files changed

+295
-67
lines changed

10 files changed

+295
-67
lines changed

config/set/downgrade-php83.php

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

55
use Rector\Config\RectorConfig;
6+
use Rector\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector;
67
use Rector\ValueObject\PhpVersion;
78
use Rector\DowngradePhp83\Rector\ClassConst\DowngradeTypedClassConstRector;
89

910
return static function (RectorConfig $rectorConfig): void {
1011
$rectorConfig->phpVersion(PhpVersion::PHP_82);
1112
$rectorConfig->rules([
1213
DowngradeTypedClassConstRector::class,
14+
DowngradeReadonlyAnonymousClassRector::class,
1315
]);
1416
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;
4+
5+
final class SkipAnonymousClass
6+
{
7+
public function run()
8+
{
9+
new readonly class {
10+
public string $foo = 'bar';
11+
};
12+
}
13+
}
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\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class DowngradeReadonlyAnonymousClassRectorTest 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector\Fixture;
4+
5+
final class Fixture
6+
{
7+
public function run()
8+
{
9+
new readonly class {
10+
public string $foo = 'bar';
11+
};
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector\Fixture;
20+
21+
final class Fixture
22+
{
23+
public function run()
24+
{
25+
new class {
26+
public readonly string $foo = 'bar';
27+
};
28+
}
29+
}
30+
31+
?>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector\Fixture;
4+
5+
final readonly class SkipNamedClass
6+
{
7+
public string $foo;
8+
}
9+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Rector\Tests\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector\Fixture;
4+
5+
final class WithParentheses
6+
{
7+
public function run()
8+
{
9+
(new readonly class {
10+
public string $foo = 'bar';
11+
});
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector\Fixture;
20+
21+
final class WithParentheses
22+
{
23+
public function run()
24+
{
25+
(new class {
26+
public readonly string $foo = 'bar';
27+
});
28+
}
29+
}
30+
31+
?>
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\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(DowngradeReadonlyAnonymousClassRector::class);
10+
};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\DowngradePhp82\NodeManipulator;
6+
7+
use PhpParser\Node\Stmt\Class_;
8+
use PhpParser\Node\Stmt\ClassMethod;
9+
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
10+
use Rector\ValueObject\MethodName;
11+
12+
final readonly class DowngradeReadonlyClassManipulator
13+
{
14+
public function __construct(
15+
private VisibilityManipulator $visibilityManipulator
16+
) {
17+
18+
}
19+
20+
public function process(Class_ $class): ?Class_
21+
{
22+
if (! $this->visibilityManipulator->isReadonly($class)) {
23+
return null;
24+
}
25+
26+
$this->visibilityManipulator->removeReadonly($class);
27+
$this->makePropertiesReadonly($class);
28+
$this->makePromotedPropertiesReadonly($class);
29+
30+
return $class;
31+
}
32+
33+
private function makePropertiesReadonly(Class_ $class): void
34+
{
35+
foreach ($class->getProperties() as $property) {
36+
if ($property->isReadonly()) {
37+
continue;
38+
}
39+
40+
/**
41+
* It technically impossible that readonly class has:
42+
*
43+
* - non-typed property
44+
* - static property
45+
*
46+
* but here to ensure no flip-flop when using direct rule for multiple rules applied
47+
*/
48+
if ($property->type === null) {
49+
continue;
50+
}
51+
52+
if ($property->isStatic()) {
53+
continue;
54+
}
55+
56+
$this->visibilityManipulator->makeReadonly($property);
57+
}
58+
}
59+
60+
private function makePromotedPropertiesReadonly(Class_ $class): void
61+
{
62+
$classMethod = $class->getMethod(MethodName::CONSTRUCT);
63+
if (! $classMethod instanceof ClassMethod) {
64+
return;
65+
}
66+
67+
foreach ($classMethod->getParams() as $param) {
68+
if ($this->visibilityManipulator->isReadonly($param)) {
69+
continue;
70+
}
71+
72+
/**
73+
* not property promotion, just param
74+
*/
75+
if ($param->flags === 0) {
76+
continue;
77+
}
78+
79+
/**
80+
* also not typed, just param
81+
*/
82+
if ($param->type === null) {
83+
continue;
84+
}
85+
86+
$this->visibilityManipulator->makeReadonly($param);
87+
}
88+
}
89+
}

rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@
66

77
use PhpParser\Node;
88
use PhpParser\Node\Stmt\Class_;
9-
use PhpParser\Node\Stmt\ClassMethod;
10-
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
9+
use Rector\DowngradePhp82\NodeManipulator\DowngradeReadonlyClassManipulator;
1110
use Rector\Rector\AbstractRector;
12-
use Rector\ValueObject\MethodName;
1311
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
1412
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
1513

@@ -21,7 +19,7 @@
2119
final class DowngradeReadonlyClassRector extends AbstractRector
2220
{
2321
public function __construct(
24-
private readonly VisibilityManipulator $visibilityManipulator
22+
private readonly DowngradeReadonlyClassManipulator $downgradeReadonlyClassManipulator
2523
) {
2624
}
2725

@@ -72,71 +70,10 @@ public function __construct()
7270
*/
7371
public function refactor(Node $node): ?Node
7472
{
75-
if (! $this->visibilityManipulator->isReadonly($node)) {
73+
if ($node->isAnonymous()) {
7674
return null;
7775
}
7876

79-
$this->visibilityManipulator->removeReadonly($node);
80-
$this->makePropertiesReadonly($node);
81-
$this->makePromotedPropertiesReadonly($node);
82-
83-
return $node;
84-
}
85-
86-
private function makePropertiesReadonly(Class_ $class): void
87-
{
88-
foreach ($class->getProperties() as $property) {
89-
if ($property->isReadonly()) {
90-
continue;
91-
}
92-
93-
/**
94-
* It technically impossible that readonly class has:
95-
*
96-
* - non-typed property
97-
* - static property
98-
*
99-
* but here to ensure no flip-flop when using direct rule for multiple rules applied
100-
*/
101-
if ($property->type === null) {
102-
continue;
103-
}
104-
105-
if ($property->isStatic()) {
106-
continue;
107-
}
108-
109-
$this->visibilityManipulator->makeReadonly($property);
110-
}
111-
}
112-
113-
private function makePromotedPropertiesReadonly(Class_ $class): void
114-
{
115-
$classMethod = $class->getMethod(MethodName::CONSTRUCT);
116-
if (! $classMethod instanceof ClassMethod) {
117-
return;
118-
}
119-
120-
foreach ($classMethod->getParams() as $param) {
121-
if ($this->visibilityManipulator->isReadonly($param)) {
122-
continue;
123-
}
124-
125-
/**
126-
* not property promotion, just param
127-
*/
128-
if ($param->flags === 0) {
129-
continue;
130-
}
131-
132-
/**
133-
* also not typed, just param
134-
*/
135-
if ($param->type === null) {
136-
continue;
137-
}
138-
139-
$this->visibilityManipulator->makeReadonly($param);
140-
}
77+
return $this->downgradeReadonlyClassManipulator->process($node);
14178
}
14279
}

0 commit comments

Comments
 (0)