From 3cae678bb00f31a2af37bb4893227033ee4000f2 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 May 2025 10:25:03 +0700 Subject: [PATCH 1/6] [Php83] Add ReadOnlyAnonymousClassRector --- config/set/php83.php | 2 + .../Fixture/extends_readonly_class.php.inc | 25 ++ .../Fixture/skip_named_class.php.inc | 8 + .../Fixture/with_anonymous_class.php.inc | 21 ++ .../ReadOnlyAnonymousClassRectorTest.php | 28 ++ .../Source/ParentAlreadyReadonly.php | 9 + .../config/configured_rule.php | 13 + .../ReadonlyClassManipulator.php | 256 ++++++++++++++++++ .../Rector/Class_/ReadOnlyClassRector.php | 238 +--------------- .../New_/ReadOnlyAnonymousClassRector.php | 78 ++++++ .../NodeManipulator/VisibilityManipulator.php | 9 +- .../Printer/BetterStandardPrinter.php | 6 + src/ValueObject/PhpVersionFeature.php | 6 + 13 files changed, 463 insertions(+), 236 deletions(-) create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php create mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/config/configured_rule.php create mode 100644 rules/Php82/NodeManipulator/ReadonlyClassManipulator.php create mode 100644 rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php diff --git a/config/set/php83.php b/config/set/php83.php index 91a264d0c02..fd3f3b1ab30 100644 --- a/config/set/php83.php +++ b/config/set/php83.php @@ -7,6 +7,7 @@ use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector; use Rector\Php83\Rector\FuncCall\CombineHostPortLdapUriRector; use Rector\Php83\Rector\FuncCall\RemoveGetClassGetParentClassNoArgsRector; +use Rector\Php83\Rector\New_\ReadOnlyAnonymousClassRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->rules([ @@ -14,5 +15,6 @@ AddTypeToConstRector::class, CombineHostPortLdapUriRector::class, RemoveGetClassGetParentClassNoArgsRector::class, + ReadOnlyAnonymousClassRector::class, ]); }; diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc new file mode 100644 index 00000000000..b13f8dcd36a --- /dev/null +++ b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc @@ -0,0 +1,25 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc new file mode 100644 index 00000000000..5d3be48044e --- /dev/null +++ b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc @@ -0,0 +1,8 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php new file mode 100644 index 00000000000..7dc1039d0bb --- /dev/null +++ b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php @@ -0,0 +1,28 @@ +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/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php new file mode 100644 index 00000000000..61eca20f2aa --- /dev/null +++ b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php @@ -0,0 +1,9 @@ +rule(ReadOnlyAnonymousClassRector::class); + + $rectorConfig->phpVersion(PhpVersionFeature::READONLY_ANONYMOUS_CLASS); +}; diff --git a/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php new file mode 100644 index 00000000000..e6d74638fa0 --- /dev/null +++ b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php @@ -0,0 +1,256 @@ +shouldSkip($node, $scope)) { + return null; + } + + $this->visibilityManipulator->makeReadonly($node); + + $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); + + if ($constructClassMethod instanceof ClassMethod) { + foreach ($constructClassMethod->getParams() as $param) { + $this->visibilityManipulator->removeReadonly($param); + + if ($param->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $param); + } + } + } + + foreach ($node->getProperties() as $property) { + $this->visibilityManipulator->removeReadonly($property); + + if ($property->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $property); + } + } + + if ($node->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $node); + } + + return $node; + } + + /** + * @return ClassReflection[] + */ + private function resolveParentClassReflections(Scope $scope): array + { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return []; + } + + return $classReflection->getParents(); + } + + /** + * @param Property[] $properties + */ + private function hasNonTypedProperty(array $properties): bool + { + foreach ($properties as $property) { + // properties of readonly class must always have type + if ($property->type === null) { + return true; + } + } + + return false; + } + + private function shouldSkip(Class_|New_ $class, Scope $scope): bool + { + $classReflection = $scope->getClassReflection(); + if (! $classReflection instanceof ClassReflection) { + return true; + } + + if ($class instanceof New_) { + $class = $class->class; + } + + if ($this->shouldSkipClass($class)) { + return true; + } + + $parents = $this->resolveParentClassReflections($scope); + + if (! $class->isAnonymous() && ! $class->isFinal()) { + return ! $this->isExtendsReadonlyClass($parents); + } + + foreach ($parents as $parent) { + if (! $parent->isReadOnly()) { + return true; + } + } + + $properties = $class->getProperties(); + if ($this->hasWritableProperty($properties)) { + return true; + } + + if ($this->hasNonTypedProperty($properties)) { + return true; + } + + if ($this->shouldSkipConsumeTraitProperty($class)) { + 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 === []; + } + + return $this->shouldSkipParams($params); + } + + private function shouldSkipConsumeTraitProperty(Class_ $class): bool + { + $traitUses = $class->getTraitUses(); + foreach ($traitUses as $traitUse) { + foreach ($traitUse->traits as $trait) { + $traitName = $trait->toString(); + + // trait not autoloaded + if (! $this->reflectionProvider->hasClass($traitName)) { + return true; + } + + $traitClassReflection = $this->reflectionProvider->getClass($traitName); + $nativeReflection = $traitClassReflection->getNativeReflection(); + + if ($this->hasReadonlyProperty($nativeReflection->getProperties())) { + return true; + } + } + } + + return false; + } + + /** + * @param ReflectionProperty[] $properties + */ + private function hasReadonlyProperty(array $properties): bool + { + foreach ($properties as $property) { + if (! $property->isReadOnly()) { + return true; + } + } + + return false; + } + + /** + * @param ClassReflection[] $parents + */ + private function isExtendsReadonlyClass(array $parents): bool + { + foreach ($parents as $parent) { + if ($parent->isReadOnly()) { + return true; + } + } + + return false; + } + + /** + * @param Property[] $properties + */ + private function hasWritableProperty(array $properties): bool + { + foreach ($properties as $property) { + if (! $property->isReadonly()) { + return true; + } + } + + return false; + } + + private function shouldSkipClass(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->phpAttributeAnalyzer->hasPhpAttribute($class, AttributeName::ALLOW_DYNAMIC_PROPERTIES)) { + return true; + } + + return $class->extends instanceof FullyQualified && ! $this->reflectionProvider->hasClass( + $class->extends->toString() + ); + } + + /** + * @param Param[] $params + */ + private function shouldSkipParams(array $params): bool + { + foreach ($params as $param) { + // has non-readonly property promotion + if (! $this->visibilityManipulator->hasVisibility($param, Visibility::READONLY) && $param->isPromoted()) { + return true; + } + + // type is missing, invalid syntax + if ($param->type === null) { + return true; + } + } + + return false; + } +} diff --git a/rules/Php82/Rector/Class_/ReadOnlyClassRector.php b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php index eeb479b1702..62b9732e084 100644 --- a/rules/Php82/Rector/Class_/ReadOnlyClassRector.php +++ b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php @@ -5,25 +5,10 @@ namespace Rector\Php82\Rector\Class_; use PhpParser\Node; -use PhpParser\Node\Name\FullyQualified; -use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; -use PhpParser\Node\Stmt\ClassMethod; -use PhpParser\Node\Stmt\Property; -use PHPStan\Analyser\Scope; -use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; -use PHPStan\Reflection\ClassReflection; -use PHPStan\Reflection\ReflectionProvider; -use Rector\NodeAnalyzer\ClassAnalyzer; -use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer; -use Rector\Php81\Enum\AttributeName; -use Rector\Php81\NodeManipulator\AttributeGroupNewLiner; -use Rector\PHPStan\ScopeFetcher; -use Rector\Privatization\NodeManipulator\VisibilityManipulator; +use Rector\Php82\NodeManipulator\ReadonlyClassManipulator; use Rector\Rector\AbstractRector; -use Rector\ValueObject\MethodName; use Rector\ValueObject\PhpVersionFeature; -use Rector\ValueObject\Visibility; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -34,11 +19,7 @@ final class ReadOnlyClassRector extends AbstractRector implements MinPhpVersionInterface { public function __construct( - private readonly ClassAnalyzer $classAnalyzer, - private readonly VisibilityManipulator $visibilityManipulator, - private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer, - private readonly ReflectionProvider $reflectionProvider, - private readonly AttributeGroupNewLiner $attributeGroupNewLiner + private readonly ReadonlyClassManipulator $readonlyClassManipulator ) { } @@ -83,226 +64,15 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { - $scope = ScopeFetcher::fetch($node); - if ($this->shouldSkip($node, $scope)) { + if ($node->isAnonymous()) { return null; } - $this->visibilityManipulator->makeReadonly($node); - - $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); - - if ($constructClassMethod instanceof ClassMethod) { - foreach ($constructClassMethod->getParams() as $param) { - $this->visibilityManipulator->removeReadonly($param); - - if ($param->attrGroups !== []) { - $this->attributeGroupNewLiner->newLine($this->file, $param); - } - } - } - - foreach ($node->getProperties() as $property) { - $this->visibilityManipulator->removeReadonly($property); - - if ($property->attrGroups !== []) { - $this->attributeGroupNewLiner->newLine($this->file, $property); - } - } - - if ($node->attrGroups !== []) { - $this->attributeGroupNewLiner->newLine($this->file, $node); - } - - return $node; + return $this->readonlyClassManipulator->processs($node, $this->file); } public function provideMinPhpVersion(): int { return PhpVersionFeature::READONLY_CLASS; } - - /** - * @return ClassReflection[] - */ - private function resolveParentClassReflections(Scope $scope): array - { - $classReflection = $scope->getClassReflection(); - if (! $classReflection instanceof ClassReflection) { - return []; - } - - return $classReflection->getParents(); - } - - /** - * @param Property[] $properties - */ - private function hasNonTypedProperty(array $properties): bool - { - foreach ($properties as $property) { - // properties of readonly class must always have type - if ($property->type === null) { - return true; - } - } - - return false; - } - - private function shouldSkip(Class_ $class, Scope $scope): bool - { - $classReflection = $scope->getClassReflection(); - if (! $classReflection instanceof ClassReflection) { - return true; - } - - if ($this->shouldSkipClass($class)) { - return true; - } - - $parents = $this->resolveParentClassReflections($scope); - if (! $class->isFinal()) { - return ! $this->isExtendsReadonlyClass($parents); - } - - foreach ($parents as $parent) { - if (! $parent->isReadOnly()) { - return true; - } - } - - $properties = $class->getProperties(); - if ($this->hasWritableProperty($properties)) { - return true; - } - - if ($this->hasNonTypedProperty($properties)) { - return true; - } - - if ($this->shouldSkipConsumeTraitProperty($class)) { - 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 === []; - } - - return $this->shouldSkipParams($params); - } - - private function shouldSkipConsumeTraitProperty(Class_ $class): bool - { - $traitUses = $class->getTraitUses(); - foreach ($traitUses as $traitUse) { - foreach ($traitUse->traits as $trait) { - $traitName = $trait->toString(); - - // trait not autoloaded - if (! $this->reflectionProvider->hasClass($traitName)) { - return true; - } - - $traitClassReflection = $this->reflectionProvider->getClass($traitName); - $nativeReflection = $traitClassReflection->getNativeReflection(); - - if ($this->hasReadonlyProperty($nativeReflection->getProperties())) { - return true; - } - } - } - - return false; - } - - /** - * @param ReflectionProperty[] $properties - */ - private function hasReadonlyProperty(array $properties): bool - { - foreach ($properties as $property) { - if (! $property->isReadOnly()) { - return true; - } - } - - return false; - } - - /** - * @param ClassReflection[] $parents - */ - private function isExtendsReadonlyClass(array $parents): bool - { - foreach ($parents as $parent) { - if ($parent->isReadOnly()) { - return true; - } - } - - return false; - } - - /** - * @param Property[] $properties - */ - private function hasWritableProperty(array $properties): bool - { - foreach ($properties as $property) { - if (! $property->isReadonly()) { - return true; - } - } - - return false; - } - - private function shouldSkipClass(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 ($this->phpAttributeAnalyzer->hasPhpAttribute($class, AttributeName::ALLOW_DYNAMIC_PROPERTIES)) { - return true; - } - - return $class->extends instanceof FullyQualified && ! $this->reflectionProvider->hasClass( - $class->extends->toString() - ); - } - - /** - * @param Param[] $params - */ - private function shouldSkipParams(array $params): bool - { - foreach ($params as $param) { - // has non-readonly property promotion - if (! $this->visibilityManipulator->hasVisibility($param, Visibility::READONLY) && $param->isPromoted()) { - return true; - } - - // type is missing, invalid syntax - if ($param->type === null) { - return true; - } - } - - return false; - } } diff --git a/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php b/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php new file mode 100644 index 00000000000..c354ed921f1 --- /dev/null +++ b/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php @@ -0,0 +1,78 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $node->isAnonymous()) { + return null; + } + + return $this->readonlyClassManipulator->processs($node, $this->file); + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::READONLY_CLASS; + } +} diff --git a/rules/Privatization/NodeManipulator/VisibilityManipulator.php b/rules/Privatization/NodeManipulator/VisibilityManipulator.php index c1b8c93d292..7faaa466b02 100644 --- a/rules/Privatization/NodeManipulator/VisibilityManipulator.php +++ b/rules/Privatization/NodeManipulator/VisibilityManipulator.php @@ -5,6 +5,7 @@ namespace Rector\Privatization\NodeManipulator; use PhpParser\Modifiers; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; @@ -128,11 +129,15 @@ public function isReadonly(Class_ | Property | Param $node): bool return $this->hasVisibility($node, Visibility::READONLY); } - public function removeReadonly(Class_ | Property | Param $node): void + public function removeReadonly(Class_ | Property | Param | New_ $node): void { $isConstructorPromotionBefore = $node instanceof Param && $node->isPromoted(); - $node->flags &= ~Modifiers::READONLY; + if ($node instanceof New_) { + $node->class->flags &= ~Modifiers::READONLY; + } else { + $node->flags &= ~Modifiers::READONLY; + } $isConstructorPromotionAfter = $node instanceof Param && $node->isPromoted(); diff --git a/src/PhpParser/Printer/BetterStandardPrinter.php b/src/PhpParser/Printer/BetterStandardPrinter.php index d178d15deb4..74e2ad19bd2 100644 --- a/src/PhpParser/Printer/BetterStandardPrinter.php +++ b/src/PhpParser/Printer/BetterStandardPrinter.php @@ -18,6 +18,7 @@ use PhpParser\Node\Expr\Instanceof_; use PhpParser\Node\Expr\Match_; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\Ternary; use PhpParser\Node\Expr\Yield_; use PhpParser\Node\InterpolatedStringPart; @@ -30,6 +31,7 @@ use PhpParser\Node\Stmt\InlineHTML; use PhpParser\Node\Stmt\Nop; use PhpParser\PrettyPrinter\Standard; +use PHPStan\Node\AnonymousClassNode; use PHPStan\Node\Expr\AlwaysRememberedExpr; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; @@ -149,6 +151,10 @@ protected function p( $content = parent::p($node, $precedence, $lhsPrecedence, $parentFormatPreserved); + if ($node instanceof New_ && $node->class instanceof AnonymousClassNode) { + $content = 'new ' . ltrim($content, 'new '); + } + return $node->getAttribute(AttributeKey::WRAPPED_IN_PARENTHESES) === true ? ('(' . $content . ')') : $content; diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index 12e1fea62dd..80c18b6e03f 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -629,6 +629,12 @@ final class PhpVersionFeature */ public const READONLY_CLASS = PhpVersion::PHP_82; + /** + * @see https://www.php.net/manual/en/migration83.new-features.php#migration83.new-features.core.readonly-modifier-improvements + * @var int + */ + public const READONLY_ANONYMOUS_CLASS = PhpVersion::PHP_83; + /** * @see https://wiki.php.net/rfc/mixed_type_v2 * @var int From 6cb89cdba77472c53eecea0ca7feb24197b81e3b Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 May 2025 10:58:54 +0700 Subject: [PATCH 2/6] Fix phpstan --- config/set/php83.php | 2 +- .../Fixture/extends_readonly_class.php.inc | 25 ------ .../Fixture/skip_named_class.php.inc | 8 -- .../Fixture/with_anonymous_class.php.inc | 21 ----- .../ReadOnlyAnonymousClassRectorTest.php | 28 ------- .../Source/ParentAlreadyReadonly.php | 9 --- .../config/configured_rule.php | 13 ---- .../ReadonlyClassManipulator.php | 8 +- .../Rector/Class_/ReadOnlyClassRector.php | 2 +- .../New_/ReadOnlyAnonymousClassRector.php | 78 ------------------- .../NodeManipulator/VisibilityManipulator.php | 6 +- 11 files changed, 7 insertions(+), 193 deletions(-) delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php delete mode 100644 rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/config/configured_rule.php delete mode 100644 rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php diff --git a/config/set/php83.php b/config/set/php83.php index fd3f3b1ab30..1a4e76d912f 100644 --- a/config/set/php83.php +++ b/config/set/php83.php @@ -3,11 +3,11 @@ declare(strict_types=1); use Rector\Config\RectorConfig; +use Rector\Php83\Rector\Class_\ReadOnlyAnonymousClassRector; use Rector\Php83\Rector\ClassConst\AddTypeToConstRector; use Rector\Php83\Rector\ClassMethod\AddOverrideAttributeToOverriddenMethodsRector; use Rector\Php83\Rector\FuncCall\CombineHostPortLdapUriRector; use Rector\Php83\Rector\FuncCall\RemoveGetClassGetParentClassNoArgsRector; -use Rector\Php83\Rector\New_\ReadOnlyAnonymousClassRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->rules([ diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc deleted file mode 100644 index b13f8dcd36a..00000000000 --- a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc +++ /dev/null @@ -1,25 +0,0 @@ - ------ - \ No newline at end of file diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc deleted file mode 100644 index 5d3be48044e..00000000000 --- a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc +++ /dev/null @@ -1,8 +0,0 @@ - ------ - \ No newline at end of file diff --git a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php deleted file mode 100644 index 7dc1039d0bb..00000000000 --- a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php +++ /dev/null @@ -1,28 +0,0 @@ -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/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php b/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php deleted file mode 100644 index 61eca20f2aa..00000000000 --- a/rules-tests/Php83/Rector/New_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php +++ /dev/null @@ -1,9 +0,0 @@ -rule(ReadOnlyAnonymousClassRector::class); - - $rectorConfig->phpVersion(PhpVersionFeature::READONLY_ANONYMOUS_CLASS); -}; diff --git a/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php index e6d74638fa0..ceddac23fd9 100644 --- a/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php +++ b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php @@ -33,7 +33,7 @@ public function __construct( ) { } - public function processs(Class_|New_ $node, File $file): Class_|New_|null + public function process(Class_ $node, File $file): Class_|null { $scope = ScopeFetcher::fetch($node); if ($this->shouldSkip($node, $scope)) { @@ -97,17 +97,13 @@ private function hasNonTypedProperty(array $properties): bool return false; } - private function shouldSkip(Class_|New_ $class, Scope $scope): bool + private function shouldSkip(Class_ $class, Scope $scope): bool { $classReflection = $scope->getClassReflection(); if (! $classReflection instanceof ClassReflection) { return true; } - if ($class instanceof New_) { - $class = $class->class; - } - if ($this->shouldSkipClass($class)) { return true; } diff --git a/rules/Php82/Rector/Class_/ReadOnlyClassRector.php b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php index 62b9732e084..41b58be53a7 100644 --- a/rules/Php82/Rector/Class_/ReadOnlyClassRector.php +++ b/rules/Php82/Rector/Class_/ReadOnlyClassRector.php @@ -68,7 +68,7 @@ public function refactor(Node $node): ?Node return null; } - return $this->readonlyClassManipulator->processs($node, $this->file); + return $this->readonlyClassManipulator->process($node, $this->file); } public function provideMinPhpVersion(): int diff --git a/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php b/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php deleted file mode 100644 index c354ed921f1..00000000000 --- a/rules/Php83/Rector/New_/ReadOnlyAnonymousClassRector.php +++ /dev/null @@ -1,78 +0,0 @@ -> - */ - public function getNodeTypes(): array - { - return [Class_::class]; - } - - /** - * @param Class_ $node - */ - public function refactor(Node $node): ?Node - { - if (! $node->isAnonymous()) { - return null; - } - - return $this->readonlyClassManipulator->processs($node, $this->file); - } - - public function provideMinPhpVersion(): int - { - return PhpVersionFeature::READONLY_CLASS; - } -} diff --git a/rules/Privatization/NodeManipulator/VisibilityManipulator.php b/rules/Privatization/NodeManipulator/VisibilityManipulator.php index 7faaa466b02..6f27f9bab7f 100644 --- a/rules/Privatization/NodeManipulator/VisibilityManipulator.php +++ b/rules/Privatization/NodeManipulator/VisibilityManipulator.php @@ -133,10 +133,10 @@ public function removeReadonly(Class_ | Property | Param | New_ $node): void { $isConstructorPromotionBefore = $node instanceof Param && $node->isPromoted(); - if ($node instanceof New_) { - $node->class->flags &= ~Modifiers::READONLY; - } else { + if (! $node instanceof New_) { $node->flags &= ~Modifiers::READONLY; + } elseif ($node->class instanceof Class_) { + $node->class->flags &= ~Modifiers::READONLY; } $isConstructorPromotionAfter = $node instanceof Param && $node->isPromoted(); From 3098d06bee9ae415a96d486a7151e198f0e24865 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 May 2025 10:59:00 +0700 Subject: [PATCH 3/6] Fix phpstan --- .../Fixture/extends_readonly_class.php.inc | 25 ++++++ .../Fixture/skip_named_class.php.inc | 8 ++ .../Fixture/with_anonymous_class.php.inc | 21 +++++ .../ReadOnlyAnonymousClassRectorTest.php | 28 +++++++ .../Source/ParentAlreadyReadonly.php | 9 +++ .../config/configured_rule.php | 13 ++++ .../Class_/ReadOnlyAnonymousClassRector.php | 78 +++++++++++++++++++ 7 files changed, 182 insertions(+) create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/config/configured_rule.php create mode 100644 rules/Php83/Rector/Class_/ReadOnlyAnonymousClassRector.php diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc new file mode 100644 index 00000000000..2a2af7073f2 --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc @@ -0,0 +1,25 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc new file mode 100644 index 00000000000..78913fb7b1b --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/skip_named_class.php.inc @@ -0,0 +1,8 @@ + +----- + \ No newline at end of file diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php new file mode 100644 index 00000000000..90e50cff133 --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/ReadOnlyAnonymousClassRectorTest.php @@ -0,0 +1,28 @@ +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/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php new file mode 100644 index 00000000000..1d2d28d9d1a --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Source/ParentAlreadyReadonly.php @@ -0,0 +1,9 @@ +rule(ReadOnlyAnonymousClassRector::class); + + $rectorConfig->phpVersion(PhpVersionFeature::READONLY_ANONYMOUS_CLASS); +}; diff --git a/rules/Php83/Rector/Class_/ReadOnlyAnonymousClassRector.php b/rules/Php83/Rector/Class_/ReadOnlyAnonymousClassRector.php new file mode 100644 index 00000000000..aaf2febd523 --- /dev/null +++ b/rules/Php83/Rector/Class_/ReadOnlyAnonymousClassRector.php @@ -0,0 +1,78 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $node->isAnonymous()) { + return null; + } + + return $this->readonlyClassManipulator->process($node, $this->file); + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::READONLY_CLASS; + } +} From 85b358a5a07e78e6999172cc1d8a38392dd89fe3 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Wed, 21 May 2025 04:00:41 +0000 Subject: [PATCH 4/6] [ci-review] Rector Rectify --- .../ReadonlyClassManipulator.php | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php index ceddac23fd9..b28f3b74a2b 100644 --- a/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php +++ b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php @@ -4,7 +4,6 @@ namespace Rector\Php82\NodeManipulator; -use PhpParser\Node\Expr\New_; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; @@ -33,16 +32,16 @@ public function __construct( ) { } - public function process(Class_ $node, File $file): Class_|null + public function process(Class_ $class, File $file): Class_|null { - $scope = ScopeFetcher::fetch($node); - if ($this->shouldSkip($node, $scope)) { + $scope = ScopeFetcher::fetch($class); + if ($this->shouldSkip($class, $scope)) { return null; } - $this->visibilityManipulator->makeReadonly($node); + $this->visibilityManipulator->makeReadonly($class); - $constructClassMethod = $node->getMethod(MethodName::CONSTRUCT); + $constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); if ($constructClassMethod instanceof ClassMethod) { foreach ($constructClassMethod->getParams() as $param) { @@ -54,7 +53,7 @@ public function process(Class_ $node, File $file): Class_|null } } - foreach ($node->getProperties() as $property) { + foreach ($class->getProperties() as $property) { $this->visibilityManipulator->removeReadonly($property); if ($property->attrGroups !== []) { @@ -62,11 +61,11 @@ public function process(Class_ $node, File $file): Class_|null } } - if ($node->attrGroups !== []) { - $this->attributeGroupNewLiner->newLine($file, $node); + if ($class->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $class); } - return $node; + return $class; } /** From b1ca9268f53e43b3b9caa1cb5660770eda804daf Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 May 2025 11:01:30 +0700 Subject: [PATCH 5/6] Fix eol --- .../Fixture/extends_readonly_class.php.inc | 2 +- .../Fixture/with_anonymous_class.php.inc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc index 2a2af7073f2..c86363ef8ed 100644 --- a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc @@ -22,4 +22,4 @@ new readonly class() extends ParentAlreadyReadonly private string $name = 'test'; }; -?> \ No newline at end of file +?> diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc index ed6c494307a..51914e275b2 100644 --- a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/with_anonymous_class.php.inc @@ -18,4 +18,4 @@ new readonly class() private string $name = 'test'; }; -?> \ No newline at end of file +?> From e7d3ed235fc06b137e16d2469035fe96f8ad7ab4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 May 2025 11:03:22 +0700 Subject: [PATCH 6/6] more fixture --- ...parenthesized_with_anonymous_class.php.inc | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/parenthesized_with_anonymous_class.php.inc diff --git a/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/parenthesized_with_anonymous_class.php.inc b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/parenthesized_with_anonymous_class.php.inc new file mode 100644 index 00000000000..a48d4a474ec --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/parenthesized_with_anonymous_class.php.inc @@ -0,0 +1,21 @@ + +----- +