diff --git a/config/set/php83.php b/config/set/php83.php index 91a264d0c02..1a4e76d912f 100644 --- a/config/set/php83.php +++ b/config/set/php83.php @@ -3,6 +3,7 @@ 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; @@ -14,5 +15,6 @@ AddTypeToConstRector::class, CombineHostPortLdapUriRector::class, RemoveGetClassGetParentClassNoArgsRector::class, + ReadOnlyAnonymousClassRector::class, ]); }; 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..c86363ef8ed --- /dev/null +++ b/rules-tests/Php83/Rector/Class_/ReadOnlyAnonymousClassRector/Fixture/extends_readonly_class.php.inc @@ -0,0 +1,25 @@ + +----- + 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 @@ + +----- + 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 @@ + +----- + 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/Php82/NodeManipulator/ReadonlyClassManipulator.php b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php new file mode 100644 index 00000000000..b28f3b74a2b --- /dev/null +++ b/rules/Php82/NodeManipulator/ReadonlyClassManipulator.php @@ -0,0 +1,251 @@ +shouldSkip($class, $scope)) { + return null; + } + + $this->visibilityManipulator->makeReadonly($class); + + $constructClassMethod = $class->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 ($class->getProperties() as $property) { + $this->visibilityManipulator->removeReadonly($property); + + if ($property->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $property); + } + } + + if ($class->attrGroups !== []) { + $this->attributeGroupNewLiner->newLine($file, $class); + } + + return $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->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..41b58be53a7 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->process($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/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; + } +} diff --git a/rules/Privatization/NodeManipulator/VisibilityManipulator.php b/rules/Privatization/NodeManipulator/VisibilityManipulator.php index c1b8c93d292..6f27f9bab7f 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->flags &= ~Modifiers::READONLY; + } elseif ($node->class instanceof Class_) { + $node->class->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