diff --git a/config/set/downgrade-php83.php b/config/set/downgrade-php83.php index 435c1707..f86f2300 100644 --- a/config/set/downgrade-php83.php +++ b/config/set/downgrade-php83.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\DowngradePhp83\Rector\Class_\DowngradeReadonlyAnonymousClassRector; use Rector\ValueObject\PhpVersion; use Rector\DowngradePhp83\Rector\ClassConst\DowngradeTypedClassConstRector; @@ -10,5 +11,6 @@ $rectorConfig->phpVersion(PhpVersion::PHP_82); $rectorConfig->rules([ DowngradeTypedClassConstRector::class, + DowngradeReadonlyAnonymousClassRector::class, ]); }; diff --git a/rules-tests/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector/Fixture/skip_anonymous_class.php.inc b/rules-tests/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector/Fixture/skip_anonymous_class.php.inc new file mode 100644 index 00000000..338ec651 --- /dev/null +++ b/rules-tests/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector/Fixture/skip_anonymous_class.php.inc @@ -0,0 +1,13 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/fixture.php.inc b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/fixture.php.inc new file mode 100644 index 00000000..a52a3f35 --- /dev/null +++ b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/fixture.php.inc @@ -0,0 +1,31 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/skip_named_class.php.inc b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/skip_named_class.php.inc new file mode 100644 index 00000000..0d0fe493 --- /dev/null +++ b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/Fixture/skip_named_class.php.inc @@ -0,0 +1,9 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/config/configured_rule.php b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/config/configured_rule.php new file mode 100644 index 00000000..774a65f9 --- /dev/null +++ b/rules-tests/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(DowngradeReadonlyAnonymousClassRector::class); +}; diff --git a/rules/DowngradePhp82/NodeManipulator/DowngradeReadonlyClassManipulator.php b/rules/DowngradePhp82/NodeManipulator/DowngradeReadonlyClassManipulator.php new file mode 100644 index 00000000..57a65374 --- /dev/null +++ b/rules/DowngradePhp82/NodeManipulator/DowngradeReadonlyClassManipulator.php @@ -0,0 +1,89 @@ +visibilityManipulator->isReadonly($class)) { + return null; + } + + $this->visibilityManipulator->removeReadonly($class); + $this->makePropertiesReadonly($class); + $this->makePromotedPropertiesReadonly($class); + + return $class; + } + + private function makePropertiesReadonly(Class_ $class): void + { + foreach ($class->getProperties() as $property) { + if ($property->isReadonly()) { + continue; + } + + /** + * It technically impossible that readonly class has: + * + * - non-typed property + * - static property + * + * but here to ensure no flip-flop when using direct rule for multiple rules applied + */ + if ($property->type === null) { + continue; + } + + if ($property->isStatic()) { + continue; + } + + $this->visibilityManipulator->makeReadonly($property); + } + } + + private function makePromotedPropertiesReadonly(Class_ $class): void + { + $classMethod = $class->getMethod(MethodName::CONSTRUCT); + if (! $classMethod instanceof ClassMethod) { + return; + } + + foreach ($classMethod->getParams() as $param) { + if ($this->visibilityManipulator->isReadonly($param)) { + continue; + } + + /** + * not property promotion, just param + */ + if ($param->flags === 0) { + continue; + } + + /** + * also not typed, just param + */ + if ($param->type === null) { + continue; + } + + $this->visibilityManipulator->makeReadonly($param); + } + } +} diff --git a/rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php b/rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php index 059b0a70..ff521b40 100644 --- a/rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php +++ b/rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php @@ -6,10 +6,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassMethod; -use Rector\Privatization\NodeManipulator\VisibilityManipulator; +use Rector\DowngradePhp82\NodeManipulator\DowngradeReadonlyClassManipulator; use Rector\Rector\AbstractRector; -use Rector\ValueObject\MethodName; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -21,7 +19,7 @@ final class DowngradeReadonlyClassRector extends AbstractRector { public function __construct( - private readonly VisibilityManipulator $visibilityManipulator + private readonly DowngradeReadonlyClassManipulator $downgradeReadonlyClassManipulator ) { } @@ -72,71 +70,10 @@ public function __construct() */ public function refactor(Node $node): ?Node { - if (! $this->visibilityManipulator->isReadonly($node)) { + if ($node->isAnonymous()) { return null; } - $this->visibilityManipulator->removeReadonly($node); - $this->makePropertiesReadonly($node); - $this->makePromotedPropertiesReadonly($node); - - return $node; - } - - private function makePropertiesReadonly(Class_ $class): void - { - foreach ($class->getProperties() as $property) { - if ($property->isReadonly()) { - continue; - } - - /** - * It technically impossible that readonly class has: - * - * - non-typed property - * - static property - * - * but here to ensure no flip-flop when using direct rule for multiple rules applied - */ - if ($property->type === null) { - continue; - } - - if ($property->isStatic()) { - continue; - } - - $this->visibilityManipulator->makeReadonly($property); - } - } - - private function makePromotedPropertiesReadonly(Class_ $class): void - { - $classMethod = $class->getMethod(MethodName::CONSTRUCT); - if (! $classMethod instanceof ClassMethod) { - return; - } - - foreach ($classMethod->getParams() as $param) { - if ($this->visibilityManipulator->isReadonly($param)) { - continue; - } - - /** - * not property promotion, just param - */ - if ($param->flags === 0) { - continue; - } - - /** - * also not typed, just param - */ - if ($param->type === null) { - continue; - } - - $this->visibilityManipulator->makeReadonly($param); - } + return $this->downgradeReadonlyClassManipulator->process($node); } } diff --git a/rules/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector.php b/rules/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector.php new file mode 100644 index 00000000..c3331579 --- /dev/null +++ b/rules/DowngradePhp83/Rector/Class_/DowngradeReadonlyAnonymousClassRector.php @@ -0,0 +1,78 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Remove "readonly" class type on anonymous class, decorate all properties to "readonly"', + [ + new CodeSample( + <<<'CODE_SAMPLE' +new readonly class +{ + public string $foo; + + public function __construct() + { + $this->foo = 'foo'; + } +}; +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +new class +{ + public readonly string $foo; + + public function __construct() + { + $this->foo = 'foo'; + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $node->isAnonymous()) { + return null; + } + + return $this->downgradeReadonlyClassManipulator->process($node); + } +}