diff --git a/config/set/level/up-to-php82.php b/config/set/level/up-to-php82.php new file mode 100644 index 00000000000..f0ac46bd709 --- /dev/null +++ b/config/set/level/up-to-php82.php @@ -0,0 +1,15 @@ +sets([SetList::PHP_82, LevelSetList::UP_TO_PHP_81]); + + // parameter must be defined after import, to override imported param version + $rectorConfig->phpVersion(PhpVersion::PHP_82); +}; diff --git a/config/set/php82.php b/config/set/php82.php new file mode 100644 index 00000000000..ea138b88af9 --- /dev/null +++ b/config/set/php82.php @@ -0,0 +1,10 @@ +rule(ReadOnlyClassRector::class); +}; diff --git a/packages/Set/ValueObject/LevelSetList.php b/packages/Set/ValueObject/LevelSetList.php index 64c417d7e36..0eb19fe0c5a 100644 --- a/packages/Set/ValueObject/LevelSetList.php +++ b/packages/Set/ValueObject/LevelSetList.php @@ -8,6 +8,11 @@ final class LevelSetList implements SetListInterface { + /** + * @var string + */ + public const UP_TO_PHP_82 = __DIR__ . '/../../../config/set/level/up-to-php82.php'; + /** * @var string */ diff --git a/packages/Set/ValueObject/SetList.php b/packages/Set/ValueObject/SetList.php index b23f4b362b1..37359fbc2bf 100644 --- a/packages/Set/ValueObject/SetList.php +++ b/packages/Set/ValueObject/SetList.php @@ -128,6 +128,11 @@ final class SetList implements SetListInterface */ public const PHP_81 = __DIR__ . '/../../../config/set/php81.php'; + /** + * @var string + */ + public const PHP_82 = __DIR__ . '/../../../config/set/php82.php'; + /** * @var string */ diff --git a/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property.php.inc b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property.php.inc new file mode 100644 index 00000000000..66c29134d1a --- /dev/null +++ b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property.php.inc @@ -0,0 +1,21 @@ + +----- + diff --git a/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property2.php.inc b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property2.php.inc new file mode 100644 index 00000000000..650ad8e847a --- /dev/null +++ b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/only_readonly_property2.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/skip_allow_dynamic.php.inc b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/skip_allow_dynamic.php.inc new file mode 100644 index 00000000000..fa6e6368a05 --- /dev/null +++ b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/Fixture/skip_allow_dynamic.php.inc @@ -0,0 +1,9 @@ +doTestFileInfo($fileInfo); + } + + /** + * @return Iterator + */ + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/config/configured_rule.php b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/config/configured_rule.php new file mode 100644 index 00000000000..ea138b88af9 --- /dev/null +++ b/rules-tests/Php82/Rector/Class_/ReadOnlyClassRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(ReadOnlyClassRector::class); +}; diff --git a/rules/Php82/Rector/Class_/ReadOnlyClassRector.php b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php new file mode 100644 index 00000000000..074d116d69b --- /dev/null +++ b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php @@ -0,0 +1,166 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if ($this->shouldSkip($node)) { + return null; + } + + $this->visibilityManipulator->makeReadonly($node); + + $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); + if ($constructClassMethod instanceof ClassMethod) { + foreach ($constructClassMethod->getParams() as $param) { + $this->visibilityManipulator->removeReadonly($param); + } + } + + foreach ($node->getProperties() as $property) { + $this->visibilityManipulator->removeReadonly($property); + } + + return $node; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::READONLY_CLASS; + } + + private function shouldSkip(Class_ $class): bool + { + // need to have test fixture once feature added to nikic/PHP-Parser + if ($this->visibilityManipulator->hasVisibility($class, Visibility::READONLY)) { + return true; + } + + if ($this->classAnalyzer->isAnonymousClass($class)) { + return true; + } + + if (! $class->isFinal()) { + return true; + } + + if ($this->phpAttributeAnalyzer->hasPhpAttribute($class, self::ATTRIBUTE)) { + return true; + } + + $properties = $class->getProperties(); + if ($this->hasWritableProperty($properties)) { + return true; + } + + $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); + if (! $constructClassMethod instanceof ClassMethod) { + // no __construct means no property promotion, skip if class has no property defined + return $properties === []; + } + + $params = $constructClassMethod->getParams(); + if ($params === []) { + // no params means no property promotion, skip if class has no property defined + return $properties === []; + } + + foreach ($params as $param) { + // has non-property promotion, skip + if (! $this->visibilityManipulator->hasVisibility($param, Visibility::READONLY)) { + return true; + } + } + + return false; + } + + /** + * @param Property[] $properties + */ + private function hasWritableProperty(array $properties): bool + { + foreach ($properties as $property) { + if (! $property->isReadonly()) { + return true; + } + } + + return false; + } +} diff --git a/rules/Privatization/NodeManipulator/VisibilityManipulator.php b/rules/Privatization/NodeManipulator/VisibilityManipulator.php index b62f4a5c778..6a947ed66ef 100644 --- a/rules/Privatization/NodeManipulator/VisibilityManipulator.php +++ b/rules/Privatization/NodeManipulator/VisibilityManipulator.php @@ -17,7 +17,7 @@ */ final class VisibilityManipulator { - public function hasVisibility(ClassMethod | Property | ClassConst | Param $node, int $visibility): bool + public function hasVisibility(Class_ | ClassMethod | Property | ClassConst | Param $node, int $visibility): bool { return (bool) ($node->flags & $visibility); } @@ -117,7 +117,7 @@ public function removeAbstract(ClassMethod $classMethod): void $classMethod->flags -= Class_::MODIFIER_ABSTRACT; } - public function makeReadonly(Property | Param $node): void + public function makeReadonly(Class_ | Property | Param $node): void { $this->addVisibilityFlag($node, Visibility::READONLY); } diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index f1f2de0cd4d..af10c45839a 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -567,6 +567,12 @@ final class PhpVersionFeature */ public const DEPRECATE_DYNAMIC_PROPERTIES = PhpVersion::PHP_82; + /** + * @see https://wiki.php.net/rfc/readonly_classes + * @var int + */ + public const READONLY_CLASS = PhpVersion::PHP_82; + /** * @see https://wiki.php.net/rfc/mixed_type_v2 * @var int