Skip to content

Commit fc92adf

Browse files
committed
feat: allow user to skip privatization for specific methods/properties
1 parent 3882cb5 commit fc92adf

File tree

7 files changed

+236
-8
lines changed

7 files changed

+236
-8
lines changed
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\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class CallbackTest 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__ . '/FixtureCallback');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule_callback.php';
27+
}
28+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\Fixture;
4+
5+
final class FinalClassWithProtectedMethods
6+
{
7+
protected function someMethod() {}
8+
9+
protected function shouldBeProtected() {}
10+
}
11+
12+
?>
13+
-----
14+
<?php
15+
16+
namespace Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\Fixture;
17+
18+
final class FinalClassWithProtectedMethods
19+
{
20+
private function someMethod() {}
21+
22+
protected function shouldBeProtected() {}
23+
}
24+
25+
?>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use PHPStan\Reflection\ClassReflection;
6+
use PhpParser\Node\Stmt\ClassMethod;
7+
use Rector\Config\RectorConfig;
8+
use Rector\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector;
9+
10+
return RectorConfig::configure()
11+
->withConfiguredRule(PrivatizeFinalClassMethodRector::class, [
12+
PrivatizeFinalClassMethodRector::SHOULD_SKIP_CALLBACK => static function (
13+
ClassMethod $classMethod,
14+
ClassReflection $classReflection,
15+
): bool {
16+
if (! str_contains($classReflection->getName(), 'FinalClassWithProtectedMethods')) {
17+
return false;
18+
}
19+
20+
21+
return str_contains($classMethod->name->toString(), 'shouldBeProtected');
22+
},
23+
]);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace Rector\Tests\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector\Fixture;
4+
5+
final class FinalClassWithProtectedProperties
6+
{
7+
protected string $value;
8+
protected string $shouldBeProtected;
9+
10+
public function __construct(
11+
protected string $bar,
12+
protected string $shouldBeProtectedAlso,
13+
) {}
14+
}
15+
16+
?>
17+
-----
18+
<?php
19+
20+
namespace Rector\Tests\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector\Fixture;
21+
22+
final class FinalClassWithProtectedProperties
23+
{
24+
private string $value;
25+
protected string $shouldBeProtected;
26+
27+
public function __construct(
28+
private string $bar,
29+
protected string $shouldBeProtectedAlso,
30+
) {}
31+
}
32+
33+
?>

rules-tests/Privatization/Rector/Property/PrivatizeFinalClassPropertyRector/config/configured_rule.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@
22

33
declare(strict_types=1);
44

5+
use PhpParser\Node\Stmt\Property;
6+
use PHPStan\Reflection\ClassReflection;
57
use Rector\Config\RectorConfig;
68
use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector;
79

810
return RectorConfig::configure()
9-
->withRules([PrivatizeFinalClassPropertyRector::class]);
11+
->withConfiguredRule(PrivatizeFinalClassPropertyRector::class, [
12+
PrivatizeFinalClassPropertyRector::SHOULD_SKIP_CALLBACK => static function (
13+
Property|string $property,
14+
ClassReflection $classReflection,
15+
): bool {
16+
if (! str_contains($classReflection->getName(), 'FinalClassWithProtectedProperties')) {
17+
return false;
18+
}
19+
20+
$name = $property instanceof Property
21+
? (string) $property->props[0]->name
22+
: $property;
23+
24+
return str_contains($name, 'shouldBeProtected');
25+
},
26+
]);

rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,33 @@
99
use PhpParser\Node\Stmt\Class_;
1010
use PhpParser\Node\Stmt\ClassMethod;
1111
use PHPStan\Reflection\ClassReflection;
12+
use Rector\Contract\Rector\ConfigurableRectorInterface;
1213
use Rector\PhpParser\Node\BetterNodeFinder;
1314
use Rector\PHPStan\ScopeFetcher;
1415
use Rector\Privatization\Guard\OverrideByParentClassGuard;
1516
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
1617
use Rector\Privatization\VisibilityGuard\ClassMethodVisibilityGuard;
1718
use Rector\Rector\AbstractRector;
18-
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
1920
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
2021

2122
/**
2223
* @see \Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\PrivatizeFinalClassMethodRectorTest
24+
* @see \Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\CallbackTest
2325
*/
24-
final class PrivatizeFinalClassMethodRector extends AbstractRector
26+
final class PrivatizeFinalClassMethodRector extends AbstractRector implements ConfigurableRectorInterface
2527
{
28+
/**
29+
* @api
30+
* @var string
31+
*/
32+
public const SHOULD_SKIP_CALLBACK = 'should_skip_callback';
33+
34+
/**
35+
* @var ?callable(ClassMethod, ClassReflection): bool
36+
*/
37+
private $shouldSkipCallback = null;
38+
2639
public function __construct(
2740
private readonly ClassMethodVisibilityGuard $classMethodVisibilityGuard,
2841
private readonly VisibilityManipulator $visibilityManipulator,
@@ -36,8 +49,14 @@ public function getRuleDefinition(): RuleDefinition
3649
return new RuleDefinition(
3750
'Change protected class method to private if possible',
3851
[
39-
new CodeSample(
52+
new ConfiguredCodeSample(
4053
<<<'CODE_SAMPLE'
54+
final class SomeOtherClass
55+
{
56+
protected function someMethod()
57+
{
58+
}
59+
}
4160
final class SomeClass
4261
{
4362
protected function someMethod()
@@ -47,18 +66,41 @@ protected function someMethod()
4766
CODE_SAMPLE
4867
,
4968
<<<'CODE_SAMPLE'
69+
final class SomeOtherClass
70+
{
71+
protected function someMethod()
72+
{
73+
}
74+
}
5075
final class SomeClass
5176
{
5277
private function someMethod()
5378
{
5479
}
5580
}
5681
CODE_SAMPLE
82+
,
83+
[
84+
self::SHOULD_SKIP_CALLBACK => static function (
85+
ClassMethod $classMethod,
86+
ClassReflection $classReflection,
87+
): bool {
88+
return $classReflection->is('SomeOtherClass');
89+
},
90+
],
5791
),
5892
]
5993
);
6094
}
6195

96+
/**
97+
* @param mixed[] $configuration
98+
*/
99+
public function configure(array $configuration): void
100+
{
101+
$this->shouldSkipCallback = $configuration[self::SHOULD_SKIP_CALLBACK] ?? null;
102+
}
103+
62104
/**
63105
* @return array<class-string<Node>>
64106
*/
@@ -107,6 +149,13 @@ public function refactor(Node $node): ?Node
107149
continue;
108150
}
109151

152+
if (
153+
is_callable($this->shouldSkipCallback)
154+
&& call_user_func($this->shouldSkipCallback, $classMethod, $classReflection)
155+
) {
156+
continue;
157+
}
158+
110159
$this->visibilityManipulator->makePrivate($classMethod);
111160
$hasChanged = true;
112161
}

rules/Privatization/Rector/Property/PrivatizeFinalClassPropertyRector.php

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,32 @@
99
use PhpParser\Node\Stmt\ClassMethod;
1010
use PhpParser\Node\Stmt\Property;
1111
use PHPStan\Reflection\ClassReflection;
12+
use Rector\Contract\Rector\ConfigurableRectorInterface;
1213
use Rector\Privatization\Guard\ParentPropertyLookupGuard;
1314
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
1415
use Rector\Rector\AbstractRector;
1516
use Rector\Reflection\ReflectionResolver;
1617
use Rector\ValueObject\MethodName;
1718
use Rector\ValueObject\Visibility;
18-
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
1920
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
2021

2122
/**
2223
* @see \Rector\Tests\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector\PrivatizeFinalClassPropertyRectorTest
2324
*/
24-
final class PrivatizeFinalClassPropertyRector extends AbstractRector
25+
final class PrivatizeFinalClassPropertyRector extends AbstractRector implements ConfigurableRectorInterface
2526
{
27+
/**
28+
* @api
29+
* @var string
30+
*/
31+
public const SHOULD_SKIP_CALLBACK = 'should_skip_callback';
32+
33+
/**
34+
* @var ?callable(Property|string, ClassReflection): bool
35+
*/
36+
private $shouldSkipCallback = null;
37+
2638
public function __construct(
2739
private readonly VisibilityManipulator $visibilityManipulator,
2840
private readonly ParentPropertyLookupGuard $parentPropertyLookupGuard,
@@ -33,24 +45,49 @@ public function __construct(
3345
public function getRuleDefinition(): RuleDefinition
3446
{
3547
return new RuleDefinition('Change property to private if possible', [
36-
new CodeSample(
48+
new ConfiguredCodeSample(
3749
<<<'CODE_SAMPLE'
50+
final class SomeOtherClass
51+
{
52+
protected $value;
53+
}
3854
final class SomeClass
3955
{
4056
protected $value;
4157
}
4258
CODE_SAMPLE
4359
,
4460
<<<'CODE_SAMPLE'
61+
final class SomeOtherClass
62+
{
63+
protected $value;
64+
}
4565
final class SomeClass
4666
{
4767
private $value;
4868
}
4969
CODE_SAMPLE
70+
,
71+
[
72+
self::SHOULD_SKIP_CALLBACK => static function (
73+
Property|string $property,
74+
ClassReflection $classReflection,
75+
): bool {
76+
return $classReflection->is('SomeOtherClass');
77+
},
78+
],
5079
),
5180
]);
5281
}
5382

83+
/**
84+
* @param mixed[] $configuration
85+
*/
86+
public function configure(array $configuration): void
87+
{
88+
$this->shouldSkipCallback = $configuration[self::SHOULD_SKIP_CALLBACK] ?? null;
89+
}
90+
5491
/**
5592
* @return array<class-string<Node>>
5693
*/
@@ -84,6 +121,13 @@ public function refactor(Node $node): ?Node
84121
continue;
85122
}
86123

124+
if (
125+
is_callable($this->shouldSkipCallback)
126+
&& call_user_func($this->shouldSkipCallback, $property, $classReflection)
127+
) {
128+
continue;
129+
}
130+
87131
$this->visibilityManipulator->makePrivate($property);
88132
$hasChanged = true;
89133
}
@@ -99,7 +143,16 @@ public function refactor(Node $node): ?Node
99143
continue;
100144
}
101145

102-
if (! $this->parentPropertyLookupGuard->isLegal((string) $this->getName($param), $classReflection)) {
146+
$property = (string) $this->getName($param);
147+
148+
if (! $this->parentPropertyLookupGuard->isLegal($property, $classReflection)) {
149+
continue;
150+
}
151+
152+
if (
153+
is_callable($this->shouldSkipCallback)
154+
&& call_user_func($this->shouldSkipCallback, $property, $classReflection)
155+
) {
103156
continue;
104157
}
105158

0 commit comments

Comments
 (0)