diff --git a/packages/CodingStyle/src/Naming/ClassNaming.php b/packages/CodingStyle/src/Naming/ClassNaming.php index 5efafaba532..b1889cece48 100644 --- a/packages/CodingStyle/src/Naming/ClassNaming.php +++ b/packages/CodingStyle/src/Naming/ClassNaming.php @@ -8,6 +8,15 @@ final class ClassNaming { public function getShortName(string $fullyQualifiedName): string { + $fullyQualifiedName = trim($fullyQualifiedName, '\\'); + return Strings::after($fullyQualifiedName, '\\', -1) ?: $fullyQualifiedName; } + + public function getNamespace(string $fullyQualifiedName): ?string + { + $fullyQualifiedName = trim($fullyQualifiedName, '\\'); + + return Strings::before($fullyQualifiedName, '\\', -1) ?: null; + } } diff --git a/src/Rector/Class_/RenameClassRector.php b/src/Rector/Class_/RenameClassRector.php index 905f8f42c82..3c9b66edf9f 100644 --- a/src/Rector/Class_/RenameClassRector.php +++ b/src/Rector/Class_/RenameClassRector.php @@ -5,13 +5,17 @@ namespace Rector\Rector\Class_; use PhpParser\Node; use PhpParser\Node\Expr\New_; use PhpParser\Node\FunctionLike; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Stmt\Class_; +use PhpParser\Node\Stmt\ClassLike; use PhpParser\Node\Stmt\Expression; +use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\Stmt\UseUse; +use Rector\CodingStyle\Naming\ClassNaming; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator; use Rector\Rector\AbstractRector; @@ -30,12 +34,26 @@ final class RenameClassRector extends AbstractRector */ private $docBlockManipulator; + /** + * @var string[] + */ + private $alreadyProcessedClasses = []; + /** + * @var ClassNaming + */ + private $classNaming; + /** * @param string[] $oldToNewClasses */ - public function __construct(DocBlockManipulator $docBlockManipulator, array $oldToNewClasses = []) + public function __construct( + DocBlockManipulator $docBlockManipulator, + ClassNaming $classNaming, + array $oldToNewClasses = [] + ) { $this->docBlockManipulator = $docBlockManipulator; + $this->classNaming = $classNaming; $this->oldToNewClasses = $oldToNewClasses; } @@ -81,7 +99,14 @@ CODE_SAMPLE */ public function getNodeTypes(): array { - return [Name::class, Property::class, FunctionLike::class, Expression::class]; + return [ + Name::class, + Property::class, + FunctionLike::class, + Expression::class, + ClassLike::class, + Namespace_::class, + ]; } /** @@ -112,6 +137,18 @@ CODE_SAMPLE return new FullyQualified($newName); } + if ($node instanceof Namespace_) { + $node = $this->refactorNamespaceNode($node); + } + + if ($node instanceof ClassLike) { + $node = $this->refactorClassLikeNode($node); + } + + if (! $node) { + return null; + } + foreach ($this->oldToNewClasses as $oldType => $newType) { $this->docBlockManipulator->changeType($node, $oldType, $newType); } @@ -176,4 +213,77 @@ CODE_SAMPLE } return ! (in_array($node, $classNode->implements, true) && class_exists($newName)); } + + private function refactorNamespaceNode(Namespace_ $node): ?Node + { + $name = $this->getName($node); + if ($name === null) { + return null; + } + + $classNode = $this->getClassOfNamespaceToRefactor($node); + if (! $classNode) { + return null; + } + + $newClassFqn = $this->oldToNewClasses[$this->getName($classNode)]; + $newNamespace = $this->classNaming->getNamespace($newClassFqn); + + // Renaming to class without namespace (example MyNamespace\DateTime -> DateTimeImmutable) + if (! $newNamespace) { + $classNode->name = new Identifier($newClassFqn); + + return $classNode; + } + + $node->name = new Name($newNamespace); + + return $node; + } + + private function getClassOfNamespaceToRefactor(Namespace_ $namespace): ?ClassLike + { + $foundClass = $this->betterNodeFinder->findFirst($namespace, function (Node $node) { + if (! $node instanceof ClassLike) { + return false; + } + + return isset($this->oldToNewClasses[$this->getName($node)]); + }); + + return $foundClass instanceof ClassLike ? $foundClass : null; + } + + private function refactorClassLikeNode(ClassLike $classLike): ?Node + { + $name = $this->getName($classLike); + if ($name === null) { + return null; + } + + $newName = $this->oldToNewClasses[$name] ?? null; + if (! $newName) { + return null; + } + + // prevents re-iterating same class in endless loop + if (in_array($name, $this->alreadyProcessedClasses, true)) { + return null; + } + + $this->alreadyProcessedClasses[] = $name; + + $newName = $this->oldToNewClasses[$name]; + $newClassNamePart = $this->classNaming->getShortName($newName); + $newNamespacePart = $this->classNaming->getNamespace($newName); + + $classLike->name = new Identifier($newClassNamePart); + + // Old class did not have any namespace, we need to wrap class with Namespace_ node + if ($newNamespacePart && ! $this->classNaming->getNamespace($name)) { + return new Namespace_(new Name($newNamespacePart), [$classLike]); + } + + return $classLike; + } } diff --git a/src/Testing/PHPUnit/AbstractRectorTestCase.php b/src/Testing/PHPUnit/AbstractRectorTestCase.php index 9119d686455..d71010dbdd8 100644 --- a/src/Testing/PHPUnit/AbstractRectorTestCase.php +++ b/src/Testing/PHPUnit/AbstractRectorTestCase.php @@ -129,6 +129,11 @@ abstract class AbstractRectorTestCase extends AbstractKernelTestCase $this->autoloadTestFixture = true; } + protected function doTestFile(string $file): void + { + $this->doTestFiles([$file]); + } + protected function getTempPath(): string { return sys_get_temp_dir() . '/rector_temp_tests'; diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_class.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class.php.inc new file mode 100644 index 00000000000..3545d85df04 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_to_class_without_namespace.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_to_class_without_namespace.php.inc new file mode 100644 index 00000000000..af5755476e0 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_to_class_without_namespace.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace.php.inc new file mode 100644 index 00000000000..4c3b7713f42 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc new file mode 100644 index 00000000000..50d05af1a2b --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_interface.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_interface.php.inc new file mode 100644 index 00000000000..92366a00652 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_interface.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/Fixture/rename_trait.php.inc b/tests/Rector/Class_/RenameClassRector/Fixture/rename_trait.php.inc new file mode 100644 index 00000000000..e1af9fc39d0 --- /dev/null +++ b/tests/Rector/Class_/RenameClassRector/Fixture/rename_trait.php.inc @@ -0,0 +1,19 @@ + +----- + diff --git a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php index 9f4063b67a0..de1cdf6d8fd 100644 --- a/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php +++ b/tests/Rector/Class_/RenameClassRector/RenameClassRectorTest.php @@ -2,6 +2,7 @@ namespace Rector\Tests\Rector\Class_\RenameClassRector; +use Iterator; use Manual\Twig\TwigFilter; use Manual_Twig_Filter; use Rector\Rector\Class_\RenameClassRector; @@ -17,17 +18,31 @@ use Rector\Tests\Rector\Class_\RenameClassRector\Source\OldClassWithTypo; */ final class RenameClassRectorTest extends AbstractRectorTestCase { - public function test(): void + /** + * @dataProvider provideTestFiles + */ + public function test(string $filePath): void { - $this->doTestFiles([ - __DIR__ . '/Fixture/class_to_new.php.inc', - __DIR__ . '/Fixture/class_to_interface.php.inc', - __DIR__ . '/Fixture/interface_to_class.php.inc', - __DIR__ . '/Fixture/name_insensitive.php.inc', - __DIR__ . '/Fixture/twig_case.php.inc', - __DIR__ . '/Fixture/underscore_doc.php.inc', - __DIR__ . '/Fixture/keep_return_tag.php.inc', - ]); + $this->doTestFile($filePath); + } + + public function provideTestFiles(): Iterator + { + yield [__DIR__ . '/Fixture/class_to_new.php.inc']; + yield [__DIR__ . '/Fixture/class_to_interface.php.inc']; + yield [__DIR__ . '/Fixture/interface_to_class.php.inc']; + yield [__DIR__ . '/Fixture/name_insensitive.php.inc']; + yield [__DIR__ . '/Fixture/twig_case.php.inc']; + yield [__DIR__ . '/Fixture/underscore_doc.php.inc']; + yield [__DIR__ . '/Fixture/keep_return_tag.php.inc']; + + // Renaming class itself and its namespace + yield [__DIR__ . '/Fixture/rename_class_without_namespace.php.inc']; + yield [__DIR__ . '/Fixture/rename_class.php.inc']; + yield [__DIR__ . '/Fixture/rename_interface.php.inc']; + yield [__DIR__ . '/Fixture/rename_trait.php.inc']; + yield [__DIR__ . '/Fixture/rename_class_without_namespace_to_class_without_namespace.php.inc']; + yield [__DIR__ . '/Fixture/rename_class_to_class_without_namespace.php.inc']; } /** @@ -44,6 +59,13 @@ final class RenameClassRectorTest extends AbstractRectorTestCase Manual_Twig_Filter::class => TwigFilter::class, 'Twig_AbstractManualExtension' => AbstractManualExtension::class, 'Twig_Extension_Sandbox' => 'Twig\Extension\SandboxExtension', + // Renaming class itself and its namespace + 'MyNamespace\MyClass' => 'MyNewNamespace\MyNewClass', + 'MyNamespace\MyTrait' => 'MyNewNamespace\MyNewTrait', + 'MyNamespace\MyInterface' => 'MyNewNamespace\MyNewInterface', + 'MyOldClass' => 'MyNamespace\MyNewClass', + 'AnotherMyOldClass' => 'AnotherMyNewClass', + 'MyNamespace\AnotherMyClass' => 'MyNewClassWithoutNamespace', ], ]]; }