diff --git a/config/level/coding-style/coding-style.yml b/config/level/coding-style/coding-style.yml index 6ca81442326..ca60c6f0cb6 100644 --- a/config/level/coding-style/coding-style.yml +++ b/config/level/coding-style/coding-style.yml @@ -6,3 +6,4 @@ services: Rector\CodingStyle\Rector\Switch_\BinarySwitchToIfElseRector: ~ Rector\CodingStyle\Rector\FuncCall\ConsistentImplodeRector: ~ Rector\CodingStyle\Rector\FuncCall\SetTypeToCastRector: ~ + Rector\CodingStyle\Rector\Use_\RemoveUnusedAliasRector: ~ diff --git a/packages/CodingStyle/src/Rector/Use_/RemoveUnusedAliasRector.php b/packages/CodingStyle/src/Rector/Use_/RemoveUnusedAliasRector.php new file mode 100644 index 00000000000..7f076eef3a2 --- /dev/null +++ b/packages/CodingStyle/src/Rector/Use_/RemoveUnusedAliasRector.php @@ -0,0 +1,159 @@ +betterNodeFinder = $betterNodeFinder; + } + + public function getDefinition(): RectorDefinition + { + return new RectorDefinition('Removes unused use aliases', [ + new CodeSample( + <<<'CODE_SAMPLE' +use Symfony\Kernel as BaseKernel; + +class SomeClass extends BaseKernel +{ +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Symfony\Kernel; + +class SomeClass extends Kernel +{ +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [Use_::class]; + } + + /** + * @param Use_ $node + */ + public function refactor(Node $node): ?Node + { + $parentNode = $node->getAttribute(Attribute::PARENT_NODE); + $usedNameNodes = $this->resolveUsedNameNodes($parentNode); + if ($usedNameNodes === []) { + return null; + } + + foreach ($node->uses as $use) { + if ($use->alias === null) { + continue; + } + + $lastName = $use->name->getLast(); + $aliasName = $this->getName($use->alias); + + // both are used → nothing to remove + if (isset($usedNameNodes[$lastName]) && isset($usedNameNodes[$aliasName])) { + continue; + } + + // only last name is used → no need for alias + if (isset($usedNameNodes[$lastName])) { + $use->alias = null; + continue; + } + + // only alis name is used → use last name directly + if (isset($usedNameNodes[$aliasName])) { + $this->renameNameNode($usedNameNodes, $aliasName, $lastName); + $use->alias = null; + } + } + + return $node; + } + + /** + * @return Name[][]|Node[][] + */ + private function resolveUsedNameNodes(Node $parentNode): array + { + $usedNameNodes = []; + + // assumption for namespaced content + if ($parentNode instanceof Namespace_) { + /** @var Name[] $namedNodes */ + $namedNodes = $this->betterNodeFinder->find($parentNode, function (Node $node) { + if ($node instanceof Name) { + return true; + } + + return false; + }); + + foreach ($namedNodes as $nameNode) { + /** node name before becoming FQN - attribute from @see NameResolver */ + $originalName = $nameNode->getAttribute('originalName'); + if (! $originalName instanceof Name) { + continue; + } + + $usedNameNodes[$originalName->toString()][] = [ + $nameNode, + $nameNode->getAttribute(Attribute::PARENT_NODE), + ]; + } + } + + return $usedNameNodes; + } + + /** + * @param Name[][]|Node[][] $usedNameNodes + */ + private function renameNameNode(array $usedNameNodes, string $aliasName, string $lastName): void + { + foreach ($usedNameNodes[$aliasName] as [$usedName, $parentNode]) { + foreach ($this->getObjectPublicPropertyNames($parentNode) as $parentNodePropertyName) { + if ($parentNode->{$parentNodePropertyName} !== $usedName) { + continue; + } + + $parentNode->{$parentNodePropertyName} = new Name($lastName); + } + } + } + + /** + * @param object $node + * @return string[] + */ + private function getObjectPublicPropertyNames($node): array + { + return array_keys(get_object_vars($node)); + } +} diff --git a/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/fixture.php.inc b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/fixture.php.inc new file mode 100644 index 00000000000..1fdafdea73c --- /dev/null +++ b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/fixture.php.inc @@ -0,0 +1,31 @@ + +----- + diff --git a/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/used.php.inc b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/used.php.inc new file mode 100644 index 00000000000..6853be6a9b9 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Fixture/used.php.inc @@ -0,0 +1,15 @@ +doTestFiles([ + __DIR__ . '/Fixture/fixture.php.inc', + __DIR__ . '/Fixture/used.php.inc', + ]); + } + + protected function getRectorClass(): string + { + return RemoveUnusedAliasRector::class; + } +} diff --git a/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Source/AbstractKernel.php b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Source/AbstractKernel.php new file mode 100644 index 00000000000..bf999035c05 --- /dev/null +++ b/packages/CodingStyle/tests/Rector/Use_/RemoveUnusedAliasRector/Source/AbstractKernel.php @@ -0,0 +1,8 @@ +addVisitor(new NameResolver()); + $nodeTraverser->addVisitor(new NameResolver(null, [ + 'preserveOriginalNames' => true, + 'replaceNodes' => true, // required by PHPStan + ])); $nodes = $nodeTraverser->traverse($nodes); $nodes = $this->nodeScopeResolver->processNodes($nodes, $filePath); $nodeTraverser = new NodeTraverser(); $nodeTraverser->addVisitor(new NameResolver(null, [ + 'preserveOriginalNames' => true, // this option would override old non-fqn-namespaced nodes otherwise, so it needs to be disabled 'replaceNodes' => false, ]));