diff --git a/README.md b/README.md index f2c63cf..7326425 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,10 @@ Insert the provided code into the specified method body at the desired position Add new imports to the file. This method will add a new import only in case it does not exist yet, preventing duplicate `use` statements. +#### removeImports + +Remove imports from the file. By default, only removes imports that are not used in the code, preventing breaking changes. Pass `true` as the second argument to force removal regardless of usage. + #### addTraits Add new `use TraitName;` statements to a class, trait, or enum. This method automatically adds the corresponding `use` imports at the top of the file and prevents duplicate trait usages. diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 25ef523..e75215c 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -19,6 +19,7 @@ use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty; +use RonasIT\Larabuilder\Visitors\RemoveImport; class PHPFileBuilder { @@ -72,6 +73,15 @@ public function addImports(array $imports): self return $this; } + public function removeImports(array $imports, bool $force = false): self + { + foreach ($imports as $import) { + $this->traverser->addVisitor(new RemoveImport($import, $force)); + } + + return $this; + } + public function addTraits(array $traits): self { $this->traverser->addVisitor(new AddTraits($traits)); diff --git a/src/Visitors/AbstractNodeVisitor.php b/src/Visitors/AbstractNodeVisitor.php index ee5b043..a9f6dfc 100644 --- a/src/Visitors/AbstractNodeVisitor.php +++ b/src/Visitors/AbstractNodeVisitor.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Enum_; +use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Trait_; use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; @@ -102,6 +103,17 @@ protected function linkParents(Node $parent): void } } + protected function &getNamespaceStatements(array &$nodes): array + { + $targetNamespace = array_find($nodes, fn ($node) => $node instanceof Namespace_); + + if (!is_null($targetNamespace)) { + return $targetNamespace->stmts; + } + + return $nodes; + } + /** @param Class_|Trait_|Enum_ $node */ private function insertNode(Node $node): Node { diff --git a/src/Visitors/AddImports.php b/src/Visitors/AddImports.php index b5142de..11659a9 100644 --- a/src/Visitors/AddImports.php +++ b/src/Visitors/AddImports.php @@ -4,7 +4,6 @@ use PhpParser\Node; use PhpParser\Node\Name; -use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; @@ -31,14 +30,7 @@ public function leaveNode(Node $node): Node public function afterTraverse(array $nodes): ?array { - $targetNamespace = array_find($nodes, fn ($node) => $node instanceof Namespace_); - - if (!is_null($targetNamespace)) { - /** @var Namespace_ $targetNamespace */ - $targetNodes = &$targetNamespace->stmts; - } else { - $targetNodes = &$nodes; - } + $targetNodes = &$this->getNamespaceStatements($nodes); $this->insertNodes($targetNodes); diff --git a/src/Visitors/RemoveImport.php b/src/Visitors/RemoveImport.php new file mode 100644 index 0000000..fa9c52d --- /dev/null +++ b/src/Visitors/RemoveImport.php @@ -0,0 +1,102 @@ +nodeFinder = new NodeFinder(); + } + + public function afterTraverse(array $nodes): ?array + { + $targetNodes = &$this->getNamespaceStatements($nodes); + + foreach ($targetNodes as $node) { + if ($node instanceof Use_ || $node instanceof GroupUse) { + $this->removeTargetImport($node, $targetNodes); + } + } + + $targetNodes = array_filter($targetNodes, fn ($node) => !$this->isEmptyImportNode($node)); + + return $nodes; + } + + protected function removeTargetImport(Use_|GroupUse $node, array $targetNodes): void + { + $prefix = $node instanceof GroupUse ? $node->prefix : null; + + $node->uses = array_filter($node->uses, fn (UseItem $useItem) => !$this->shouldRemove($useItem, $targetNodes, $prefix)); + } + + protected function shouldRemove(UseItem $useItem, array $targetNodes, ?Name $prefix = null): bool + { + if ($this->resolveFqcn($useItem, $prefix) !== $this->import) { + return false; + } + + if ($this->force) { + return true; + } + + $resolvedName = $useItem->alias?->name ?? $useItem->name->getLast(); + + return !$this->isImportUsed($resolvedName, $targetNodes); + } + + protected function resolveFqcn(UseItem $useItem, ?Name $prefix): string + { + return $prefix !== null + ? $prefix->toString() . '\\' . $useItem->name->toString() + : $useItem->name->toString(); + } + + protected function isEmptyImportNode(Node $node): bool + { + return ($node instanceof Use_ || $node instanceof GroupUse) && empty($node->uses); + } + + protected function isImportUsed(string $importName, array $targetNodes): bool + { + $nodesWithoutImports = array_filter($targetNodes, fn ($node) => !($node instanceof Use_) && !($node instanceof GroupUse)); + + if ($this->hasUsageOf($importName, $nodesWithoutImports)) { + return true; + } + + $preformattedNodes = $this->nodeFinder->find($nodesWithoutImports, fn (Node $node) => $node instanceof PreformattedCode); + + foreach ($preformattedNodes as $preformattedNode) { + /** @var PreformattedCode $preformattedNode */ + if ($this->hasUsageOf($importName, $preformattedNode->code)) { + return true; + } + } + + return false; + } + + protected function hasUsageOf(string $name, array $nodes): bool + { + return !empty($this->nodeFinder->findFirst( + $nodes, + fn (Node $node) => $node instanceof Name && get_class($node) === Name::class && $node->getFirst() === $name, + )); + } +} diff --git a/tests/NodeInserterTest.php b/tests/NodeInserterTest.php index fd1278f..9502fb3 100644 --- a/tests/NodeInserterTest.php +++ b/tests/NodeInserterTest.php @@ -48,7 +48,7 @@ public function testInsertMixedNodes(): void new TraitUse([new Name('NewTrait')]), new ClassConst([new Const_('ANOTHER_CONST', new Int_(0))], Modifiers::PUBLIC), new TraitUse([new Name('AnotherTrait')]), - ], true); + ]); $this->assertSame( $this->getFixture('class_with_mixed_nodes_inserted.php'), diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index b302fa4..58d4a1d 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -514,7 +514,7 @@ public static function provideInsertDuplicateCode(): array 'code' => '$db->table(\'users\')->where(\'id\', 1)->first();', ], [ - 'code' => 'Arr::map($arr, fn ($value) => str_replace(\'0\', \'1\', $value));', + 'code' => 'Helpers\Arr::map($arr, fn ($value) => str_replace(\'0\', \'1\', $value));', ], ]; } @@ -724,4 +724,102 @@ public function testRemoveMethod(string $structure, string $method, string $resu ->removeMethod($method) ->save(); } + + public function testRemoveImportsUnused(): void + { + $file = $this->generateOriginalStructurePath('class.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'remove_unused_import.php'), + ); + + new PHPFileBuilder($file) + ->removeImports([ + 'App\Service\UserService', + 'Some\SomeTrait', + 'Some\AnotherTrait', + 'App\Service\UserService', + 'App\Support\Traits\SecondTrait', + ]) + ->save(); + } + + public function testRemoveImportsUsedSkipped(): void + { + $file = $this->generateOriginalStructurePath('class.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'class_unchanged.php'), + ); + + new PHPFileBuilder($file) + ->removeImports([ + 'RonasIT\Support\Traits\FirstTrait', + 'RonasIT\Support\Traits\SecondTrait', + ]) + ->save(); + } + + public function testRemoveImportsForce(): void + { + $file = $this->generateOriginalStructurePath('class.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'remove_imports_force.php'), + ); + + new PHPFileBuilder($file) + ->removeImports([ + 'RonasIT\Larabuilder\Tests\Support\FirstClass', + 'Some\SomeTrait', + 'RonasIT\Support\Traits\FirstTrait', + 'App\Service\UserService', + 'App\Support\Traits\SecondTrait', + 'App\Support\Classname', + 'Illuminate\Support', + ], force: true) + ->save(); + } + + public function testRemoveImportsAfterChanges(): void + { + $file = $this->generateOriginalStructurePath('class.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'remove_imports_after_changes.php'), + ); + + new PHPFileBuilder($file) + ->insertCodeToMethod('someMethod', 'app(UserService::class)->doSomething();') + ->removeImports([ + 'App\Service\UserService', + 'App\Support\Classname', + ]) + ->save(); + } + + public function testRemoveImportsThenAddImports(): void + { + $file = $this->generateOriginalStructurePath('class.php'); + + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFilePutContent($file, 'remove_then_add_imports.php'), + ); + + new PHPFileBuilder($file) + ->removeImports([ + 'RonasIT\Support\SecondTrait', + 'RonasIT\Support\Traits\NewTrait', + 'App\Support\Traits\SecondTrait', + 'App\Support\Classname', + 'Illuminate\Support', + ], force: true) + ->addImports(['App\New\Service']) + ->save(); + } } diff --git a/tests/Support/OriginStructures/class.php b/tests/Support/OriginStructures/class.php index 9e82639..a67de0c 100644 --- a/tests/Support/OriginStructures/class.php +++ b/tests/Support/OriginStructures/class.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -31,6 +36,8 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } diff --git a/tests/fixtures/NodeInserterTest/class_with_mixed_nodes_inserted.php b/tests/fixtures/NodeInserterTest/class_with_mixed_nodes_inserted.php index 083ccda..bebead7 100644 --- a/tests/fixtures/NodeInserterTest/class_with_mixed_nodes_inserted.php +++ b/tests/fixtures/NodeInserterTest/class_with_mixed_nodes_inserted.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -39,7 +44,9 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } public function newMethod() diff --git a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php index 71458e1..c4c5811 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; use RonasIT\Larabuilder\Tests\Support\SecondClass; use RonasIT\Larabuilder\Tests\Support\ThirdClass; @@ -33,6 +38,8 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } diff --git a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php index b3c7958..09076f7 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; use RonasIT\Support\Traits\SecondTrait; use RonasIT\Support\Traits\ThirdTrait; @@ -34,6 +39,8 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } diff --git a/tests/fixtures/PHPFileBuilderTest/class_method_removed.php b/tests/fixtures/PHPFileBuilderTest/class_method_removed.php index 6c5a607..1094d42 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_method_removed.php +++ b/tests/fixtures/PHPFileBuilderTest/class_method_removed.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test diff --git a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php index 9e82639..a67de0c 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php +++ b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -31,6 +36,8 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } diff --git a/tests/fixtures/PHPFileBuilderTest/class_with_added_protected_method.php b/tests/fixtures/PHPFileBuilderTest/class_with_added_protected_method.php index cb76da3..d91a937 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_added_protected_method.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_added_protected_method.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -31,7 +36,9 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } protected function boot() diff --git a/tests/fixtures/PHPFileBuilderTest/class_with_added_static_method.php b/tests/fixtures/PHPFileBuilderTest/class_with_added_static_method.php index 2217618..8b2f1c9 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_added_static_method.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_added_static_method.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -31,7 +36,9 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } public static function create(): static diff --git a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php index ce233c5..f6d33de 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php @@ -3,8 +3,13 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; +use Some\{SomeTrait, AnotherTrait}; use RonasIT\Support\Traits\FirstTrait; +use App\Service\UserService; +use RonasIT\Support\SecondTrait; +use RonasIT\Support\Traits\NewTrait as SomeTrait; +use App\Support\Traits\SecondTrait as UnusedTrait, App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -33,6 +38,8 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); - Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php new file mode 100644 index 0000000..b711b91 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php @@ -0,0 +1,45 @@ +save(); + + $config = ['status' => true, 'version' => 1]; + + $db->table('users')->where('id', 1)->first(); + + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; + + app(UserService::class)->doSomething(); + } +} diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php new file mode 100644 index 0000000..d45ba67 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php @@ -0,0 +1,38 @@ +save(); + + $config = ['status' => true, 'version' => 1]; + + $db->table('users')->where('id', 1)->first(); + + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; + } +} diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_from_class_unchanged.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_from_class_unchanged.php new file mode 100644 index 0000000..ceae17b --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_from_class_unchanged.php @@ -0,0 +1,25 @@ +save(); + + $config = ['status' => true, 'version' => 1]; + + $db->table('users')->where('id', 1)->first(); + + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; + } +} diff --git a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php new file mode 100644 index 0000000..4025ffc --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php @@ -0,0 +1,41 @@ +save(); + + $config = ['status' => true, 'version' => 1]; + + $db->table('users')->where('id', 1)->first(); + + Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; + } +}