Added support for renaming class and it’s namespace

This commit is contained in:
Jan Mikeš 2019-05-03 16:14:53 +02:00
parent fac1503915
commit 9958a44ec3
No known key found for this signature in database
GPG Key ID: 1DEDF63B40DDA99D
10 changed files with 328 additions and 12 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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';

View File

@ -0,0 +1,33 @@
<?php
namespace MyNamespace;
class MyClass
{
/**
* @return MyClass
*/
public function createSelf(): MyClass
{
return new MyClass;
}
}
?>
-----
<?php
namespace MyNewNamespace;
class MyNewClass
{
/**
* @return \MyNewNamespace\MyNewClass
*/
public function createSelf(): \MyNewNamespace\MyNewClass
{
return new \MyNewNamespace\MyNewClass;
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
namespace MyNamespace;
class AnotherMyClass
{
/**
* @return AnotherMyClass
*/
public function createSelf(): AnotherMyClass
{
return new AnotherMyClass;
}
}
?>
-----
<?php
class MyNewClassWithoutNamespace
{
/**
* @return MyNewClassWithoutNamespace
*/
public function createSelf(): \MyNewClassWithoutNamespace
{
return new \MyNewClassWithoutNamespace;
}
}
?>

View File

@ -0,0 +1,31 @@
<?php
class MyOldClass
{
/**
* @return MyOldClass
*/
public function createSelf(): MyOldClass
{
return new MyOldClass;
}
}
?>
-----
<?php
namespace MyNamespace;
class MyNewClass
{
/**
* @return \MyNamespace\MyNewClass
*/
public function createSelf(): \MyNamespace\MyNewClass
{
return new \MyNamespace\MyNewClass;
}
}
?>

View File

@ -0,0 +1,29 @@
<?php
class AnotherMyOldClass
{
/**
* @return AnotherMyOldClass
*/
public function createSelf(): AnotherMyOldClass
{
return new AnotherMyOldClass;
}
}
?>
-----
<?php
class AnotherMyNewClass
{
/**
* @return AnotherMyNewClass
*/
public function createSelf(): \AnotherMyNewClass
{
return new \AnotherMyNewClass;
}
}
?>

View File

@ -0,0 +1,27 @@
<?php
namespace MyNamespace;
interface MyInterface
{
/**
* @return MyInterface
*/
public function createSelf(): MyInterface;
}
?>
-----
<?php
namespace MyNewNamespace;
interface MyNewInterface
{
/**
* @return \MyNewNamespace\MyNewInterface
*/
public function createSelf(): \MyNewNamespace\MyNewInterface;
}
?>

View File

@ -0,0 +1,19 @@
<?php
namespace MyNamespace;
trait MyTrait
{
}
?>
-----
<?php
namespace MyNewNamespace;
trait MyNewTrait
{
}
?>

View File

@ -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',
],
]];
}