From 263b26c99cc4cd684dd11b8c33868fffff79ed57 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 18 Oct 2026 16:08:17 +0200 Subject: [PATCH 1/2] [perf] Skip unused-import name resolution when the node has no use statement UnusedImportRemovingPostRector::enterNode() fires for both the FileNode root and the inner Namespace_. To find which imports are unused it resolves every referenced name via two full-AST traversals (findNonUseImportNames + findNamesInDocBlocks), and that resolution is not cached. In a namespaced file the `use` statements live inside the Namespace_, so the FileNode-level call did the full double walk and then found no `use` statement to act on - pure waste. Add a cheap guard: if the node has no direct `use` statement there is nothing to remove, so return before the resolution walks. Behavior is unchanged (non-namespaced files keep their `use` at FileNode level and still resolve). Adds bench/unused_import_removing_bench.php. --- bench/unused_import_removing_bench.php | 98 +++++++++++++++++++ .../Rector/UnusedImportRemovingPostRector.php | 19 ++++ 2 files changed, 117 insertions(+) create mode 100644 bench/unused_import_removing_bench.php diff --git a/bench/unused_import_removing_bench.php b/bench/unused_import_removing_bench.php new file mode 100644 index 00000000000..2cb5e010edc --- /dev/null +++ b/bench/unused_import_removing_bench.php @@ -0,0 +1,98 @@ +create(); +$rectorConfig->boot(); + +/** @var TestingParser $testingParser */ +$testingParser = $rectorConfig->make(TestingParser::class); + +$sourceFilePath = __DIR__ . '/Source/ManyNamesSource.php'; + +/** @var UnusedImportRemovingPostRector $postRector */ +$postRector = $rectorConfig->make(UnusedImportRemovingPostRector::class); + +$cpuSeconds = static function (): float { + $usage = getrusage(); + return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000 + + $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000; +}; + +$file = $testingParser->parseFilePathToFile($sourceFilePath); +$fileNode = $file->getFileNode(); +assert($fileNode instanceof FileNode); + +// the root nodes enterNode() actually hits per file: the FileNode and the inner Namespace_ +$rootNodes = [$fileNode]; +foreach ($fileNode->stmts as $stmt) { + if ($stmt instanceof Namespace_) { + $rootNodes[] = $stmt; + } +} + +$run = static function () use ($postRector, $rootNodes): void { + for ($enter = 0; $enter < ENTERS; ++$enter) { + foreach ($rootNodes as $rootNode) { + $postRector->enterNode($rootNode); + } + } +}; + +$measure = static function () use ($run, $cpuSeconds): array { + // warm up autoloading / parser once, excluded from timing + $run(); + + $cpuStart = $cpuSeconds(); + $wallStart = hrtime(true); + + for ($repeat = 0; $repeat < REPEAT; ++$repeat) { + $run(); + } + + return [ + 'cpu' => $cpuSeconds() - $cpuStart, + 'wall' => (hrtime(true) - $wallStart) / 1_000_000_000, + ]; +}; + +assert(class_exists(File::class)); + +$result = $measure(); + +printf('UnusedImportRemovingPostRector::enterNode benchmark%s', PHP_EOL); +printf(' root nodes=%d enters=%d repeat=%d%s', count($rootNodes), ENTERS, REPEAT, PHP_EOL); +printf('%s', PHP_EOL); +printf(' enterNode cpu=%.4fs wall=%.4fs%s', $result['cpu'], $result['wall'], PHP_EOL); diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index 120c30b3520..468d7a70e14 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -36,6 +36,11 @@ public function enterNode(Node $node): ?Node return null; } + // no use import at this level → nothing to remove, skip the full name resolution walks + if (! $this->hasUseStmt($node->stmts)) { + return null; + } + $hasChanged = false; $namespaceOriginalCase = $node instanceof Namespace_ && $node->name instanceof Name ? $node->name->toString() @@ -98,6 +103,20 @@ public function enterNode(Node $node): ?Node return $node; } + /** + * @param Node[] $stmts + */ + private function hasUseStmt(array $stmts): bool + { + foreach ($stmts as $stmt) { + if ($stmt instanceof Use_) { + return true; + } + } + + return false; + } + /** * @return string[] */ From 343e10b78b73bf3273e5638c6f3a27799100b1a0 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 18 Oct 2026 14:09:30 +0000 Subject: [PATCH 2/2] [ci-review] Rector Rectify --- src/PostRector/Rector/UnusedImportRemovingPostRector.php | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index 468d7a70e14..f45fc2f640a 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -108,13 +108,7 @@ public function enterNode(Node $node): ?Node */ private function hasUseStmt(array $stmts): bool { - foreach ($stmts as $stmt) { - if ($stmt instanceof Use_) { - return true; - } - } - - return false; + return array_any($stmts, fn(Node $stmt): bool => $stmt instanceof Use_); } /**