From 6b15192d1b9cab1573891e77fb35ff1c03449093 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sun, 18 Oct 2026 16:56:32 +0200 Subject: [PATCH] [perf] Cache resolved used names per version in UnusedImportRemovingPostRector To find unused `use` imports, enterNode() resolves every referenced name via two full-AST traversals (findNonUseImportNames + findNamesInDocBlocks), and that resolution was recomputed on every call. The post-rector chain re-runs this node across the file convergence iterations, so on iterations where the stmts did not change the same names were resolved again from scratch. Cache the resolved names per (file, stmts version): a new version re-resolves, an unchanged one reuses the previous result. The cache is reset when the file changes, so it never leaks across files or serves a stale map. Extends bench/unused_import_removing_bench.php (sets the file so the cached path is exercised). --- bench/unused_import_removing_bench.php | 2 ++ .../Rector/UnusedImportRemovingPostRector.php | 33 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/bench/unused_import_removing_bench.php b/bench/unused_import_removing_bench.php index 2cb5e010edc..503a789e5b7 100644 --- a/bench/unused_import_removing_bench.php +++ b/bench/unused_import_removing_bench.php @@ -52,6 +52,8 @@ }; $file = $testingParser->parseFilePathToFile($sourceFilePath); +$postRector->setFile($file); + $fileNode = $file->getFileNode(); assert($fileNode instanceof FileNode); diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index f45fc2f640a..d3ef58d3c3d 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -21,9 +21,17 @@ use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; use Rector\PhpParser\Node\FileNode; +use Rector\ValueObject\Application\File; final class UnusedImportRemovingPostRector extends AbstractPostRector { + private ?File $usedNamesFile = null; + + /** + * @var array + */ + private array $usedNamesCache = []; + public function __construct( private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser, private readonly PhpDocInfoFactory $phpDocInfoFactory, @@ -198,11 +206,32 @@ private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array */ private function resolveUsedPhpAndDocNames(Namespace_|FileNode $rootNode): array { + // the referenced names only change with a new stmts version, while the post-rector chain + // re-runs this node across convergence iterations, so cache per (file, version) + $file = $this->getFile(); + if ($file !== $this->usedNamesFile) { + $this->usedNamesFile = $file; + $this->usedNamesCache = []; + } + + $version = $file->getNewStmtsVersion(); + $objectId = spl_object_id($rootNode); + + $cached = $this->usedNamesCache[$objectId] ?? null; + if ($cached !== null && $cached['version'] === $version) { + return $cached['names']; + } + $phpNames = $this->findNonUseImportNames($rootNode); $docBlockNames = $this->findNamesInDocBlocks($rootNode); - $names = [...$phpNames, ...$docBlockNames]; - return array_unique($names); + $names = array_unique([...$phpNames, ...$docBlockNames]); + $this->usedNamesCache[$objectId] = [ + 'version' => $version, + 'names' => $names, + ]; + + return $names; } /**