Skip to content

Commit b9fef0f

Browse files
authored
Add unused import remover to cleanup use imports after changes (rectorphp#3358)
1 parent 638a0ad commit b9fef0f

16 files changed

+219
-60
lines changed

config/config.php

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363

6464
// to avoid autoimporting out of the box
6565
$rectorConfig->importNames(false, false);
66+
$rectorConfig->removeUnusedImports(false);
6667

6768
$rectorConfig->importShortClasses();
6869
$rectorConfig->indent(' ', 4);

packages/BetterPhpDocParser/PhpDocInfo/PhpDocInfo.php

+18
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
1717
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
1818
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
19+
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1920
use PHPStan\PhpDocParser\Lexer\Lexer;
2021
use PHPStan\Type\MixedType;
2122
use PHPStan\Type\Type;
@@ -430,6 +431,23 @@ public function getNode(): \PhpParser\Node
430431
return $this->node;
431432
}
432433

434+
/**
435+
* @return string[]
436+
*/
437+
public function getAnnotationClassNames(): array
438+
{
439+
/** @var IdentifierTypeNode[] $identifierTypeNodes */
440+
$identifierTypeNodes = $this->phpDocNodeByTypeFinder->findByType($this->phpDocNode, IdentifierTypeNode::class);
441+
442+
$resolvedClasses = [];
443+
444+
foreach ($identifierTypeNodes as $identifierTypeNode) {
445+
$resolvedClasses[] = ltrim($identifierTypeNode->name, '@');
446+
}
447+
448+
return $resolvedClasses;
449+
}
450+
433451
private function resolveNameForPhpDocTagValueNode(PhpDocTagValueNode $phpDocTagValueNode): ?string
434452
{
435453
foreach (self::TAGS_TYPES_TO_NAMES as $tagValueNodeType => $name) {

packages/Config/RectorConfig.php

+6
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ public function skip(array $criteria): void
6969
$parameters->set(Option::SKIP, $criteria);
7070
}
7171

72+
public function removeUnusedImports(bool $removeUnusedImports = true): void
73+
{
74+
$parameters = $this->parameters();
75+
$parameters->set(Option::REMOVE_UNUSED_IMPORTS, $removeUnusedImports);
76+
}
77+
7278
public function importNames(bool $importNames = true, bool $importDocBlockNames = true): void
7379
{
7480
$parameters = $this->parameters();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PostRector\Rector;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Name;
9+
use PhpParser\Node\Stmt\Namespace_;
10+
use PhpParser\Node\Stmt\Use_;
11+
use PhpParser\NodeTraverser;
12+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
13+
use Rector\Core\Configuration\RectorConfigProvider;
14+
use Rector\NodeTypeResolver\Node\AttributeKey;
15+
use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser;
16+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
17+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
18+
19+
final class UnusedImportRemovingPostRector extends AbstractPostRector
20+
{
21+
public function __construct(
22+
private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser,
23+
private readonly PhpDocInfoFactory $phpDocInfoFactory,
24+
private readonly RectorConfigProvider $rectorConfigProvider,
25+
) {
26+
}
27+
28+
public function enterNode(Node $node): ?Node
29+
{
30+
if (! $this->rectorConfigProvider->shouldRemoveUnusedImports()) {
31+
return null;
32+
}
33+
34+
if (! $node instanceof Namespace_) {
35+
return null;
36+
}
37+
38+
$hasChanged = false;
39+
40+
$names = $this->resolveUsedPhpAndDocNames($node);
41+
42+
foreach ($node->stmts as $key => $namespaceStmt) {
43+
if (! $namespaceStmt instanceof Use_) {
44+
continue;
45+
}
46+
47+
$useUse = $namespaceStmt->uses[0];
48+
// skip aliased imports, harder to check
49+
if ($useUse->alias !== null) {
50+
continue;
51+
}
52+
53+
$comparedName = $useUse->name->toString();
54+
if ($this->isUseImportUsed($comparedName, $names)) {
55+
continue;
56+
}
57+
58+
unset($node->stmts[$key]);
59+
$hasChanged = true;
60+
}
61+
62+
if ($hasChanged === false) {
63+
return null;
64+
}
65+
66+
$node->stmts = array_values($node->stmts);
67+
return $node;
68+
}
69+
70+
/**
71+
* The higher, the later
72+
*/
73+
public function getPriority(): int
74+
{
75+
// run this last
76+
return 100;
77+
}
78+
79+
public function getRuleDefinition(): RuleDefinition
80+
{
81+
return new RuleDefinition('Removes unused import names', [
82+
new CodeSample(
83+
<<<'CODE_SAMPLE'
84+
namespace App;
85+
86+
use App\SomeUnusedClass;
87+
88+
class SomeClass
89+
{
90+
}
91+
CODE_SAMPLE
92+
,
93+
<<<'CODE_SAMPLE'
94+
namespace App;
95+
96+
class SomeClass
97+
{
98+
}
99+
CODE_SAMPLE
100+
),
101+
]);
102+
}
103+
104+
/**
105+
* @return string[]
106+
*/
107+
private function findNonUseImportNames(Namespace_ $namespace): array
108+
{
109+
$names = [];
110+
111+
$this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace, static function (Node $node) use (
112+
&$names
113+
) {
114+
if ($node instanceof Use_) {
115+
return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
116+
}
117+
118+
if (! $node instanceof Name) {
119+
return null;
120+
}
121+
122+
$names[] = $node->toString();
123+
return $node;
124+
});
125+
126+
return $names;
127+
}
128+
129+
/**
130+
* @return string[]
131+
*/
132+
private function findNamesInDocBlocks(Namespace_ $namespace): array
133+
{
134+
$names = [];
135+
136+
$this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace, function (Node $node) use (
137+
&$names
138+
) {
139+
if (! $node->hasAttribute(AttributeKey::COMMENTS)) {
140+
return null;
141+
}
142+
143+
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
144+
$names = array_merge($names, $phpDocInfo->getAnnotationClassNames());
145+
});
146+
147+
return $names;
148+
}
149+
150+
/**
151+
* @return string[]
152+
*/
153+
private function resolveUsedPhpAndDocNames(Namespace_ $namespace): array
154+
{
155+
$phpNames = $this->findNonUseImportNames($namespace);
156+
$docBlockNames = $this->findNamesInDocBlocks($namespace);
157+
158+
return array_merge($phpNames, $docBlockNames);
159+
}
160+
161+
/**
162+
* @param string[] $names
163+
*/
164+
private function isUseImportUsed(string $comparedName, array $names): bool
165+
{
166+
if (in_array($comparedName, $names, true)) {
167+
return true;
168+
}
169+
170+
// match partial import
171+
foreach ($names as $name) {
172+
if (str_ends_with($comparedName, $name)) {
173+
return true;
174+
}
175+
}
176+
177+
return false;
178+
}
179+
}

rules-tests/Renaming/Rector/Name/RenameClassRector/AutoImportNamesPhp74Test.php

-31
This file was deleted.

rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/do_not_implement_twice.php.inc

-3
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@ final class DoNotImplementTwice implements FirstInterface, SecondInterface
1515

1616
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
1717

18-
use Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\Contract\FirstInterface;
19-
use Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\Contract\SecondInterface;
20-
2118
final class DoNotImplementTwice implements \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\Contract\ThirdInterface
2219
{
2320
}

rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/do_not_remove_existing_target_namespace.php.inc

-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,6 @@ namespace Acme\Bar {
3737
<?php
3838

3939
namespace Acme {
40-
use Acme\Foo\DoNotUpdateExistingTargetNamespace;
41-
4240
\Acme\Bar\DoNotUpdateExistingTargetNamespace::run();
4341
}
4442

rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/name_insensitive.php.inc

-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ class NameInsensitive extends OldClassWithTypO
1818

1919
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
2020

21-
use Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\OldClassWithTypo;
22-
2321
class NameInsensitive extends \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\NewClassWithoutTypo
2422
{
2523
public function run(): \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\NewClassWithoutTypo

rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/rename_generic_template_of_fixture.php.inc

-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ interface MyServiceInterface
2121

2222
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
2323

24-
use MyNamespace\MyClass;
25-
2624
/**
2725
* @template T of \MyNewNamespace\MyNewClass
2826
*/

rules-tests/Renaming/Rector/Name/RenameClassRector/Fixture/rename_property_read.php.inc

-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ class RenamePropertyRead
1919

2020
namespace Rector\Tests\Renaming\Rector\Name\RenameClassRector\Fixture;
2121

22-
use Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\OldClass;
23-
2422
/**
2523
* @property \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\NewClass $some
2624
* @property-read \Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\NewClass $someRead

rules-tests/Renaming/Rector/Name/RenameClassRector/FixtureAutoImportNamesPhp74/skip_already_in_use.php.inc

-16
This file was deleted.

rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceExceptionSuffixCallback.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use PHPStan\Reflection\ReflectionProvider;
1010
use Rector\NodeNameResolver\NodeNameResolver;
1111

12-
class EnforceExceptionSuffixCallback
12+
final class EnforceExceptionSuffixCallback
1313
{
1414
public function __invoke(
1515
ClassLike $class,

rules-tests/Renaming/Rector/Name/RenameClassRector/Source/EnforceInterfaceSuffixCallback.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use PhpParser\Node\Stmt\Interface_;
99
use Rector\NodeNameResolver\NodeNameResolver;
1010

11-
class EnforceInterfaceSuffixCallback
11+
final class EnforceInterfaceSuffixCallback
1212
{
1313
public function __invoke(ClassLike $class, NodeNameResolver $nodeNameResolver): ?string {
1414
$fullyQualifiedClassName = (string) $nodeNameResolver->getName($class);

rules-tests/Renaming/Rector/Name/RenameClassRector/config/configured_rule.php

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Rector\Tests\Renaming\Rector\Name\RenameClassRector\Source\SomeNonFinalClass;
2020

2121
return static function (RectorConfig $rectorConfig): void {
22+
$rectorConfig->removeUnusedImports();
23+
2224
$rectorConfig
2325
->ruleWithConfiguration(RenameClassRector::class, [
2426
'FqnizeNamespaced' => 'Abc\FqnizeNamespaced',

src/Configuration/Option.php

+6
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,10 @@ final class Option
212212
* @var string
213213
*/
214214
public const INDENT_SIZE = 'indent-size';
215+
216+
/**
217+
* @internal Use @see \Rector\Config\RectorConfig::removeUnusedImports() method
218+
* @var string
219+
*/
220+
public const REMOVE_UNUSED_IMPORTS = 'remove-unused-imports';
215221
}

src/Configuration/RectorConfigProvider.php

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public function shouldImportNames(): bool
2222
return $this->parameterProvider->provideBoolParameter(Option::AUTO_IMPORT_NAMES);
2323
}
2424

25+
public function shouldRemoveUnusedImports(): bool
26+
{
27+
return $this->parameterProvider->provideBoolParameter(Option::REMOVE_UNUSED_IMPORTS);
28+
}
29+
2530
/**
2631
* @api symfony
2732
*/

0 commit comments

Comments
 (0)