From adf12040149429dfbd78ca77d404aa3bd778548c Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 10 Apr 2026 10:22:14 +0300 Subject: [PATCH 1/7] feat: implement removeImports method refs: https://github.com/RonasIT/larabuilder/issues/60 --- src/Builders/PHPFileBuilder.php | 8 ++ src/Visitors/AddImports.php | 10 +-- src/Visitors/BaseNodeVisitorAbstract.php | 11 +++ src/Visitors/RemoveImports.php | 79 +++++++++++++++++++ tests/PHPFileBuilderTest.php | 69 ++++++++++++++++ .../original/remove_imports_from_class.php | 24 ++++++ .../results/remove_imports_after_changes.php | 25 ++++++ .../results/remove_imports_force.php | 20 +++++ .../remove_imports_from_class_unchanged.php | 24 ++++++ .../results/remove_unused_import.php | 23 ++++++ 10 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 src/Visitors/RemoveImports.php create mode 100644 tests/fixtures/PHPFileBuilderTest/original/remove_imports_from_class.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/remove_imports_after_changes.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/remove_imports_force.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/remove_imports_from_class_unchanged.php create mode 100644 tests/fixtures/PHPFileBuilderTest/results/remove_unused_import.php diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index 0f9128e..e0a3687 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -16,6 +16,7 @@ use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty; +use RonasIT\Larabuilder\Visitors\RemoveImports; class PHPFileBuilder { @@ -69,6 +70,13 @@ public function addImports(array $imports): self return $this; } + public function removeImports(array $imports, bool $force = false): self + { + $this->traverser->addVisitor(new RemoveImports($imports, $force)); + + return $this; + } + public function addTraits(array $traits): self { $this->traverser->addVisitor(new AddTraits($traits)); diff --git a/src/Visitors/AddImports.php b/src/Visitors/AddImports.php index 40f6b61..b11054a 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\Stmt\UseUse; @@ -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/BaseNodeVisitorAbstract.php b/src/Visitors/BaseNodeVisitorAbstract.php index 1bfba7f..e42288f 100644 --- a/src/Visitors/BaseNodeVisitorAbstract.php +++ b/src/Visitors/BaseNodeVisitorAbstract.php @@ -204,4 +204,15 @@ protected function isSubsequence(array $haystackStatements, array $needleStateme return false; } + + protected function &getNamespaceStatements(array &$nodes): array + { + $targetNamespace = array_find($nodes, fn ($node) => $node instanceof Namespace_); + + if (!is_null($targetNamespace)) { + return $targetNamespace->stmts; + } + + return $nodes; + } } diff --git a/src/Visitors/RemoveImports.php b/src/Visitors/RemoveImports.php new file mode 100644 index 0000000..5df6cfd --- /dev/null +++ b/src/Visitors/RemoveImports.php @@ -0,0 +1,79 @@ +nodeFinder = new NodeFinder(); + } + + public function afterTraverse(array $nodes): ?array + { + $targetNodes = &$this->getNamespaceStatements($nodes); + + foreach ($targetNodes as $node) { + if ($node instanceof Use_) { + $node->uses = array_filter($node->uses, fn (UseItem $useItem) => !$this->shouldRemove($useItem, $targetNodes)); + } + } + + $targetNodes = array_filter($targetNodes, fn ($node) => !($node instanceof Use_) || !empty($node->uses)); + + return $nodes; + } + + protected function shouldRemove(UseItem $useItem, array $targetNodes): bool + { + if (!in_array($useItem->name->toString(), $this->imports)) { + return false; + } + + if ($this->force) { + return true; + } + + $resolvedName = $useItem->alias?->name ?? $useItem->name->getLast(); + + return !$this->isImportUsed($resolvedName, $targetNodes); + } + + protected function isImportUsed(string $importName, array $targetNodes): bool + { + $nodesWithoutImports = array_filter($targetNodes, fn ($node) => !($node instanceof Use_)); + + 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->find($nodes, fn (Node $node) => $node instanceof Name && $node->toString() === $name)); + } +} diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index 387e8ef..800ba85 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -507,6 +507,75 @@ public function testInsertCodeToMethodNotClassTraitEnum(): void ->save(); } + public function testRemoveImportsUnused(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'remove_imports_from_class.php'), + $this->callFilePutContent('some_file_path.php', 'remove_unused_import.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeImports([ + 'App\Service\UserService', + 'App\Support\Traits\SecondTrait', + ]) + ->save(); + } + + public function testRemoveImportsUsedSkipped(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'remove_imports_from_class.php'), + $this->callFilePutContent('some_file_path.php', 'remove_imports_from_class_unchanged.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeImports([ + 'App\SomeClass', + 'App\Models\User', + 'RonasIT\Support\Traits\SecondTrait', + ]) + ->save(); + } + + public function testRemoveImportsForce(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'remove_imports_from_class.php'), + $this->callFilePutContent('some_file_path.php', 'remove_imports_force.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->removeImports([ + 'App\SomeClass', + 'App\Models\User', + 'App\Support\Traits\SecondTrait', + 'App\Service\UserService', + 'App\Support\Classname', + ], force: true) + ->save(); + } + + public function testRemoveImportsAfterChanges(): void + { + $this->mockNativeFunction( + 'RonasIT\Larabuilder\Builders', + $this->callFileGetContent('some_file_path.php', 'remove_imports_from_class.php'), + $this->callFilePutContent('some_file_path.php', 'remove_imports_after_changes.php'), + ); + + new PHPFileBuilder('some_file_path.php') + ->insertCodeToMethod('someMethod', 'app(UserService::class)->doSomething();') + ->removeImports([ + 'App\Service\UserService', + 'App\Support\Classname', + ]) + ->save(); + } + public function testInsertCodeToMethodWhenMethodNotExist(): void { $this->mockNativeFunction( diff --git a/tests/fixtures/PHPFileBuilderTest/original/remove_imports_from_class.php b/tests/fixtures/PHPFileBuilderTest/original/remove_imports_from_class.php new file mode 100644 index 0000000..b2eade0 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/original/remove_imports_from_class.php @@ -0,0 +1,24 @@ +doSomething(); + } +} \ No newline at end of file diff --git a/tests/fixtures/PHPFileBuilderTest/results/remove_imports_force.php b/tests/fixtures/PHPFileBuilderTest/results/remove_imports_force.php new file mode 100644 index 0000000..061f960 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/results/remove_imports_force.php @@ -0,0 +1,20 @@ + Date: Fri, 10 Apr 2026 22:51:37 +0300 Subject: [PATCH 2/7] docs: add removeImports to README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 11ff580..7bd163c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,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. From 8de82a8221255a7fa57a73ba0ef87a559ca74ee9 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 09:19:47 +0300 Subject: [PATCH 3/7] fix: detect alias prefix usage in hasUsageOf --- src/Visitors/RemoveImports.php | 2 +- tests/PHPFileBuilderTest.php | 3 ++- tests/Support/OriginStructures/class.php | 3 ++- tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php | 3 ++- tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php | 3 ++- tests/fixtures/PHPFileBuilderTest/class_unchanged.php | 3 ++- .../fixtures/PHPFileBuilderTest/class_with_new_properties.php | 3 ++- .../PHPFileBuilderTest/remove_imports_after_changes.php | 3 ++- tests/fixtures/PHPFileBuilderTest/remove_imports_force.php | 2 +- tests/fixtures/PHPFileBuilderTest/remove_unused_import.php | 3 ++- 10 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Visitors/RemoveImports.php b/src/Visitors/RemoveImports.php index 5df6cfd..7874e2e 100644 --- a/src/Visitors/RemoveImports.php +++ b/src/Visitors/RemoveImports.php @@ -74,6 +74,6 @@ protected function isImportUsed(string $importName, array $targetNodes): bool protected function hasUsageOf(string $name, array $nodes): bool { - return !empty($this->nodeFinder->find($nodes, fn (Node $node) => $node instanceof Name && $node->toString() === $name)); + return !empty($this->nodeFinder->find($nodes, fn (Node $node) => $node instanceof Name && $node->getFirst() === $name)); } } diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index 670e4d3..e38faf0 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -511,7 +511,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));', ], ]; } @@ -582,6 +582,7 @@ public function testRemoveImportsForce(): void 'App\Service\UserService', 'App\Support\Traits\SecondTrait', 'App\Support\Classname', + 'Illuminate\Support', ], force: true) ->save(); } diff --git a/tests/Support/OriginStructures/class.php b/tests/Support/OriginStructures/class.php index a72a577..784ab3d 100644 --- a/tests/Support/OriginStructures/class.php +++ b/tests/Support/OriginStructures/class.php @@ -9,6 +9,7 @@ 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 @@ -35,6 +36,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php index 687d3e5..3a6e81f 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php @@ -9,6 +9,7 @@ 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; @@ -37,6 +38,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php index b00efc9..d743e4e 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php @@ -9,6 +9,7 @@ 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; @@ -38,6 +39,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php index a72a577..784ab3d 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php +++ b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php @@ -9,6 +9,7 @@ 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 @@ -35,6 +36,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php index d243827..9a40540 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php @@ -9,6 +9,7 @@ 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 @@ -37,6 +38,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php index 63c172d..2c2d975 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php @@ -9,6 +9,7 @@ use RonasIT\Support\SecondTrait; use RonasIT\Support\Traits\NewTrait as SomeTrait; use App\Support\Traits\SecondTrait as UnusedTrait; +use Illuminate\Support as Helpers; /** * Test @@ -35,7 +36,7 @@ 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)); app(UserService::class)->doSomething(); } diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php index 35a8a16..a61c2d0 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php @@ -30,6 +30,6 @@ 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)); } } diff --git a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php index cb46e29..255e66f 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php @@ -8,6 +8,7 @@ use RonasIT\Support\SecondTrait; use RonasIT\Support\Traits\NewTrait as SomeTrait; use App\Support\Classname; +use Illuminate\Support as Helpers; /** * Test @@ -34,6 +35,6 @@ 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)); } } From 92ef39f19e0527d47e21e8d694e6d5e7bdc0a3d7 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 13:46:35 +0300 Subject: [PATCH 4/7] feat: support grouped imports and alias prefix usage in RemoveImports --- src/Visitors/RemoveImports.php | 32 +++++++++++++++---- tests/PHPFileBuilderTest.php | 3 ++ tests/Support/OriginStructures/class.php | 2 +- .../add_imports_to_class.php | 2 +- .../add_traits_to_class.php | 2 +- .../PHPFileBuilderTest/class_unchanged.php | 2 +- .../class_with_new_properties.php | 2 +- .../remove_imports_after_changes.php | 2 +- .../remove_imports_force.php | 1 + .../remove_unused_import.php | 1 - 10 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Visitors/RemoveImports.php b/src/Visitors/RemoveImports.php index 7874e2e..481c0fb 100644 --- a/src/Visitors/RemoveImports.php +++ b/src/Visitors/RemoveImports.php @@ -4,6 +4,7 @@ use PhpParser\Node; use PhpParser\Node\Name; +use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; use PhpParser\NodeFinder; @@ -27,19 +28,26 @@ public function afterTraverse(array $nodes): ?array $targetNodes = &$this->getNamespaceStatements($nodes); foreach ($targetNodes as $node) { - if ($node instanceof Use_) { - $node->uses = array_filter($node->uses, fn (UseItem $useItem) => !$this->shouldRemove($useItem, $targetNodes)); + if ($node instanceof Use_ || $node instanceof GroupUse) { + $this->removeTargetImports($node, $targetNodes); } } - $targetNodes = array_filter($targetNodes, fn ($node) => !($node instanceof Use_) || !empty($node->uses)); + $targetNodes = array_filter($targetNodes, fn ($node) => !$this->isEmptyImportNode($node)); return $nodes; } - protected function shouldRemove(UseItem $useItem, array $targetNodes): bool + protected function removeTargetImports(Use_|GroupUse $node, array $targetNodes): void { - if (!in_array($useItem->name->toString(), $this->imports)) { + $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 (!in_array($this->resolveFqcn($useItem, $prefix), $this->imports)) { return false; } @@ -52,9 +60,21 @@ protected function shouldRemove(UseItem $useItem, array $targetNodes): bool 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_)); + $nodesWithoutImports = array_filter($targetNodes, fn ($node) => !($node instanceof Use_) && !($node instanceof GroupUse)); if ($this->hasUsageOf($importName, $nodesWithoutImports)) { return true; diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index e38faf0..a39ebc9 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -542,6 +542,9 @@ public function testRemoveImportsUnused(): void new PHPFileBuilder($file) ->removeImports([ + 'App\Service\UserService', + 'Some\SomeTrait', + 'Some\AnotherTrait', 'App\Service\UserService', 'App\Support\Traits\SecondTrait', ]) diff --git a/tests/Support/OriginStructures/class.php b/tests/Support/OriginStructures/class.php index 784ab3d..53d31e0 100644 --- a/tests/Support/OriginStructures/class.php +++ b/tests/Support/OriginStructures/class.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php index 3a6e81f..462d09a 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php index d743e4e..947084d 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php index 784ab3d..53d31e0 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php +++ b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php index 9a40540..77c59d1 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php index 2c2d975..682f5de 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php @@ -3,7 +3,7 @@ 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; diff --git a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php index a61c2d0..0bdb803 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php @@ -2,6 +2,7 @@ namespace RonasIT\Larabuilder\Tests\Support; +use Some\{AnotherTrait}; use RonasIT\Support\SecondTrait; use RonasIT\Support\Traits\NewTrait as SomeTrait; diff --git a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php index 255e66f..bd4811a 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php @@ -3,7 +3,6 @@ namespace RonasIT\Larabuilder\Tests\Support; use RonasIT\Larabuilder\Tests\Support\FirstClass; -use Some\SomeTrait; use RonasIT\Support\Traits\FirstTrait; use RonasIT\Support\SecondTrait; use RonasIT\Support\Traits\NewTrait as SomeTrait; From a21e20ba62fce104eaafed8ceeb0135185f8cfce Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 14:25:33 +0300 Subject: [PATCH 5/7] feat: fix hasUsageOf to ignore fully-qualified names and support grouped imports --- src/Visitors/RemoveImports.php | 5 ++++- tests/Support/OriginStructures/class.php | 2 ++ tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php | 2 ++ tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php | 2 ++ tests/fixtures/PHPFileBuilderTest/class_unchanged.php | 2 ++ .../PHPFileBuilderTest/class_with_new_properties.php | 2 ++ .../PHPFileBuilderTest/remove_imports_after_changes.php | 2 ++ tests/fixtures/PHPFileBuilderTest/remove_imports_force.php | 2 ++ tests/fixtures/PHPFileBuilderTest/remove_unused_import.php | 2 ++ 9 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Visitors/RemoveImports.php b/src/Visitors/RemoveImports.php index 481c0fb..ae8c06a 100644 --- a/src/Visitors/RemoveImports.php +++ b/src/Visitors/RemoveImports.php @@ -94,6 +94,9 @@ protected function isImportUsed(string $importName, array $targetNodes): bool protected function hasUsageOf(string $name, array $nodes): bool { - return !empty($this->nodeFinder->find($nodes, fn (Node $node) => $node instanceof Name && $node->getFirst() === $name)); + return !empty($this->nodeFinder->find( + $nodes, + fn (Node $node) => $node instanceof Name && get_class($node) === Name::class && $node->getFirst() === $name, + )); } } diff --git a/tests/Support/OriginStructures/class.php b/tests/Support/OriginStructures/class.php index 53d31e0..a67de0c 100644 --- a/tests/Support/OriginStructures/class.php +++ b/tests/Support/OriginStructures/class.php @@ -37,5 +37,7 @@ public function someMethod() $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/add_imports_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php index 462d09a..c4c5811 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_imports_to_class.php @@ -39,5 +39,7 @@ public function someMethod() $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/add_traits_to_class.php b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php index 947084d..09076f7 100644 --- a/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php +++ b/tests/fixtures/PHPFileBuilderTest/add_traits_to_class.php @@ -40,5 +40,7 @@ public function someMethod() $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/class_unchanged.php b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php index 53d31e0..a67de0c 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_unchanged.php +++ b/tests/fixtures/PHPFileBuilderTest/class_unchanged.php @@ -37,5 +37,7 @@ public function someMethod() $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/class_with_new_properties.php b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php index 77c59d1..f6d33de 100644 --- a/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php +++ b/tests/fixtures/PHPFileBuilderTest/class_with_new_properties.php @@ -39,5 +39,7 @@ public function someMethod() $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_after_changes.php b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php index 682f5de..b711b91 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_after_changes.php @@ -38,6 +38,8 @@ public function someMethod() 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 index 0bdb803..d45ba67 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_imports_force.php @@ -32,5 +32,7 @@ public function someMethod() $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 index bd4811a..4025ffc 100644 --- a/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php +++ b/tests/fixtures/PHPFileBuilderTest/remove_unused_import.php @@ -35,5 +35,7 @@ public function someMethod() $db->table('users')->where('id', 1)->first(); Helpers\Arr::map($arr, fn ($value) => str_replace('0', '1', $value)); + + $x = \App\Service\UserService::CONST; } } From 19e86cb28eae249790a17fac69fdfa060ce253d2 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Mon, 20 Apr 2026 14:36:29 +0300 Subject: [PATCH 6/7] test: add testRemoveImportsThenAddImports to verify correct import ordering after chaining --- tests/PHPFileBuilderTest.php | 21 ++++++++++ .../remove_then_add_imports.php | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 tests/fixtures/PHPFileBuilderTest/remove_then_add_imports.php diff --git a/tests/PHPFileBuilderTest.php b/tests/PHPFileBuilderTest.php index a39ebc9..59e00dd 100644 --- a/tests/PHPFileBuilderTest.php +++ b/tests/PHPFileBuilderTest.php @@ -607,4 +607,25 @@ public function testRemoveImportsAfterChanges(): void ]) ->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/fixtures/PHPFileBuilderTest/remove_then_add_imports.php b/tests/fixtures/PHPFileBuilderTest/remove_then_add_imports.php new file mode 100644 index 0000000..3012ae2 --- /dev/null +++ b/tests/fixtures/PHPFileBuilderTest/remove_then_add_imports.php @@ -0,0 +1,40 @@ +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; + } +} From e6475ddf93f8ca906979737b74183ccf225d2c97 Mon Sep 17 00:00:00 2001 From: Artyom Osepyan Date: Fri, 22 May 2026 07:43:39 +0300 Subject: [PATCH 7/7] fix: remarks from reviewer --- src/Builders/PHPFileBuilder.php | 6 ++++-- src/Visitors/{RemoveImports.php => RemoveImport.php} | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) rename src/Visitors/{RemoveImports.php => RemoveImport.php} (88%) diff --git a/src/Builders/PHPFileBuilder.php b/src/Builders/PHPFileBuilder.php index e0a3687..709a78b 100644 --- a/src/Builders/PHPFileBuilder.php +++ b/src/Builders/PHPFileBuilder.php @@ -16,7 +16,7 @@ use RonasIT\Larabuilder\Visitors\PropertyVisitors\AddArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\RemoveArrayPropertyItem; use RonasIT\Larabuilder\Visitors\PropertyVisitors\SetProperty; -use RonasIT\Larabuilder\Visitors\RemoveImports; +use RonasIT\Larabuilder\Visitors\RemoveImport; class PHPFileBuilder { @@ -72,7 +72,9 @@ public function addImports(array $imports): self public function removeImports(array $imports, bool $force = false): self { - $this->traverser->addVisitor(new RemoveImports($imports, $force)); + foreach ($imports as $import) { + $this->traverser->addVisitor(new RemoveImport($import, $force)); + } return $this; } diff --git a/src/Visitors/RemoveImports.php b/src/Visitors/RemoveImport.php similarity index 88% rename from src/Visitors/RemoveImports.php rename to src/Visitors/RemoveImport.php index 2d11540..fa9c52d 100644 --- a/src/Visitors/RemoveImports.php +++ b/src/Visitors/RemoveImport.php @@ -10,14 +10,14 @@ use PhpParser\NodeFinder; use RonasIT\Larabuilder\Nodes\PreformattedCode; -class RemoveImports extends AbstractNodeVisitor +class RemoveImport extends AbstractNodeVisitor { protected array $allowedParentNodesTypes = self::ANY_TYPE; protected NodeFinder $nodeFinder; public function __construct( - protected array $imports, + protected string $import, protected bool $force = false, ) { $this->nodeFinder = new NodeFinder(); @@ -29,7 +29,7 @@ public function afterTraverse(array $nodes): ?array foreach ($targetNodes as $node) { if ($node instanceof Use_ || $node instanceof GroupUse) { - $this->removeTargetImports($node, $targetNodes); + $this->removeTargetImport($node, $targetNodes); } } @@ -38,7 +38,7 @@ public function afterTraverse(array $nodes): ?array return $nodes; } - protected function removeTargetImports(Use_|GroupUse $node, array $targetNodes): void + protected function removeTargetImport(Use_|GroupUse $node, array $targetNodes): void { $prefix = $node instanceof GroupUse ? $node->prefix : null; @@ -47,7 +47,7 @@ protected function removeTargetImports(Use_|GroupUse $node, array $targetNodes): protected function shouldRemove(UseItem $useItem, array $targetNodes, ?Name $prefix = null): bool { - if (!in_array($this->resolveFqcn($useItem, $prefix), $this->imports)) { + if ($this->resolveFqcn($useItem, $prefix) !== $this->import) { return false; } @@ -94,7 +94,7 @@ protected function isImportUsed(string $importName, array $targetNodes): bool protected function hasUsageOf(string $name, array $nodes): bool { - return !empty($this->nodeFinder->find( + return !empty($this->nodeFinder->findFirst( $nodes, fn (Node $node) => $node instanceof Name && get_class($node) === Name::class && $node->getFirst() === $name, ));