From c11bb00fd047be6ebf5964dddc13fb92d9003a8c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 16 Jun 2026 22:23:00 +0200 Subject: [PATCH 1/8] move use import to file --- .../Application/UseImportsAdder.php | 89 +++++-------------- src/Application/FileProcessor.php | 17 +++- src/PhpParser/Node/FileNode.php | 23 +++++ src/PostRector/Rector/UseAddingPostRector.php | 32 +++---- 4 files changed, 69 insertions(+), 92 deletions(-) diff --git a/rules/CodingStyle/Application/UseImportsAdder.php b/rules/CodingStyle/Application/UseImportsAdder.php index 1e1cc7c6529..6ad7d88a847 100644 --- a/rules/CodingStyle/Application/UseImportsAdder.php +++ b/rules/CodingStyle/Application/UseImportsAdder.php @@ -11,7 +11,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Use_; -use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; +use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; use Rector\PhpParser\Node\FileNode; @@ -21,47 +21,45 @@ final readonly class UseImportsAdder { public function __construct( - private UsedImportsResolver $usedImportsResolver, private TypeFactory $typeFactory ) { } /** - * @param Stmt[] $stmts * @param array $useImportTypes * @param array $constantUseImportTypes * @param array $functionUseImportTypes */ public function addImportsToStmts( - FileNode $fileNode, - array $stmts, + FileNode|Namespace_ $node, + UsedImports $usedImports, array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes ): bool { - $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); - $existingUseImportTypes = $usedImports->getUseImports(); - $existingConstantUseImports = $usedImports->getConstantImports(); - $existingFunctionUseImports = $usedImports->getFunctionImports(); + $namespaceName = $node instanceof Namespace_ ? $this->getNamespaceName($node) : null; + + $existingUseImportTypes = $this->typeFactory->uniquateTypes($usedImports->getUseImports()); $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes( $constantUseImportTypes, - $existingConstantUseImports + $usedImports->getConstantImports() ); $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes( $functionUseImportTypes, - $existingFunctionUseImports + $usedImports->getFunctionImports() ); - $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, null); + $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $namespaceName); if ($newUses === []) { return false; } - $stmts = array_values(array_filter($stmts, static function (Stmt $stmt): bool { + // remove empty use stmts + $node->stmts = array_values(array_filter($node->stmts, static function (Stmt $stmt): bool { if (! $stmt instanceof Use_) { return true; } @@ -70,7 +68,7 @@ public function addImportsToStmts( })); // place after declare strict_types - foreach ($stmts as $key => $stmt) { + foreach ($node->stmts as $key => $stmt) { // maybe just added a space if ($stmt instanceof Nop) { continue; @@ -83,72 +81,25 @@ public function addImportsToStmts( $nodesToAdd = array_merge([new Nop()], $newUses); - $this->mirrorUseComments($stmts, $newUses, $key + 1); + $this->mirrorUseComments($node->stmts, $newUses, $key + 1); // remove space before next use tweak - if (isset($stmts[$key + 1]) && ($stmts[$key + 1] instanceof Use_ || $stmts[$key + 1] instanceof GroupUse)) { - $stmts[$key + 1]->setAttribute(AttributeKey::ORIGINAL_NODE, null); + if (isset($node->stmts[$key + 1]) && ($node->stmts[$key + 1] instanceof Use_ || $node->stmts[$key + 1] instanceof GroupUse)) { + $node->stmts[$key + 1]->setAttribute(AttributeKey::ORIGINAL_NODE, null); } - array_splice($stmts, $key + 1, 0, $nodesToAdd); + array_splice($node->stmts, $key + 1, 0, $nodesToAdd); - $fileNode->stmts = $stmts; - $fileNode->stmts = array_values($fileNode->stmts); + $node->stmts = array_values($node->stmts); return true; } - $this->mirrorUseComments($stmts, $newUses); + $this->mirrorUseComments($node->stmts, $newUses); // make use stmts first - $fileNode->stmts = array_merge($newUses, $this->resolveInsertNop($fileNode), $stmts); - $fileNode->stmts = array_values($fileNode->stmts); - - return true; - } - - /** - * @param FullyQualifiedObjectType[] $useImportTypes - * @param FullyQualifiedObjectType[] $constantUseImportTypes - * @param FullyQualifiedObjectType[] $functionUseImportTypes - */ - public function addImportsToNamespace( - Namespace_ $namespace, - array $useImportTypes, - array $constantUseImportTypes, - array $functionUseImportTypes - ): bool { - $namespaceName = $this->getNamespaceName($namespace); - - $existingUsedImports = $this->usedImportsResolver->resolveForStmts($namespace->stmts); - $existingUseImportTypes = $existingUsedImports->getUseImports(); - $existingConstantUseImportTypes = $existingUsedImports->getConstantImports(); - $existingFunctionUseImportTypes = $existingUsedImports->getFunctionImports(); - - $existingUseImportTypes = $this->typeFactory->uniquateTypes($existingUseImportTypes); - - $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); - - $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes( - $constantUseImportTypes, - $existingConstantUseImportTypes - ); - - $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes( - $functionUseImportTypes, - $existingFunctionUseImportTypes - ); - - $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $namespaceName); - - if ($newUses === []) { - return false; - } - - $this->mirrorUseComments($namespace->stmts, $newUses); - - $namespace->stmts = array_merge($newUses, $this->resolveInsertNop($namespace), $namespace->stmts); - $namespace->stmts = array_values($namespace->stmts); + $node->stmts = array_merge($newUses, $this->resolveInsertNop($node), $node->stmts); + $node->stmts = array_values($node->stmts); return true; } diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index fa9e863e4d4..39fbbf4d7ae 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -10,6 +10,7 @@ use Rector\Caching\Detector\ChangedFilesDetector; use Rector\ChangesReporting\ValueObjectFactory\ErrorFactory; use Rector\ChangesReporting\ValueObjectFactory\FileDiffFactory; +use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; use Rector\Exception\ShouldNotHappenException; use Rector\FileSystem\FilePathHelper; use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator; @@ -40,6 +41,7 @@ public function __construct( private PostFileProcessor $postFileProcessor, private RectorParser $rectorParser, private NodeScopeAndMetadataDecorator $nodeScopeAndMetadataDecorator, + private UsedImportsResolver $usedImportsResolver, ) { } @@ -61,13 +63,20 @@ public function processFile(File $file, Configuration $configuration): FileProce // 1. change nodes with Rector Rules $newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts()); - // 2. apply post rectors + // 2. refresh used imports on the FileNode from current stmts, + // so post rectors read them without re-traversing, yet stay in sync each iteration + $fileNode = $newStmts[0] ?? null; + if ($fileNode instanceof FileNode) { + $fileNode->setUsedImports($this->usedImportsResolver->resolveForStmts($fileNode->stmts)); + } + + // 3. apply post rectors $postNewStmts = $this->postFileProcessor->traverse($newStmts, $file); - // 3. this is needed for new tokens added in "afterTraverse()" + // 4. this is needed for new tokens added in "afterTraverse()" $file->changeNewStmts($postNewStmts); - // 4. print to file or string + // 5. print to file or string // important to detect if file has changed $this->printFile($file, $configuration, $filePath); @@ -79,7 +88,7 @@ public function processFile(File $file, Configuration $configuration): FileProce $fileHasChanged = true; } while (true); - // 5. add as cacheable if not changed at all + // 6. add as cacheable if not changed at all if (! $fileHasChanged) { $this->changedFilesDetector->addCacheableFile($filePath); } else { diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index be23ed4a3b5..505d257a43a 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -9,12 +9,19 @@ use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; +use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; +use Rector\Exception\ShouldNotHappenException; /** * Inspired by https://github.com/phpstan/phpstan-src/commit/ed81c3ad0b9877e6122c79b4afda9d10f3994092 */ class FileNode extends Stmt { + /** + * Resolved once on file parse, so used imports do not have to be re-traversed later + */ + private ?UsedImports $usedImports = null; + /** * @param Stmt[] $stmts */ @@ -27,6 +34,22 @@ public function __construct( parent::__construct($attributes); } + public function setUsedImports(UsedImports $usedImports): void + { + $this->usedImports = $usedImports; + } + + public function getUsedImports(): UsedImports + { + if (! $this->usedImports instanceof UsedImports) { + throw new ShouldNotHappenException( + 'Used imports are not set yet; call setUsedImports() on file parse first.' + ); + } + + return $this->usedImports; + } + /** * This triggers Printed method with "pFileNode" name * @see \Rector\PhpParser\Printer\BetterStandardPrinter::pStmt_FileNode() diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index 7b9e70784f0..89d895c63b0 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -9,6 +9,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeVisitor; use Rector\CodingStyle\Application\UseImportsAdder; +use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; use Rector\PhpParser\Node\FileNode; use Rector\PostRector\Collector\UseNodesToAddCollector; @@ -39,6 +40,10 @@ public function beforeTraverse(array $nodes): array return $nodes; } + // the used imports are resolved once on file parse and stored on the FileNode root + /** @var FileNode $fileNode */ + $fileNode = $nodes[0]; + $useImportTypes = $this->useNodesToAddCollector->getObjectImportsByFilePath($this->getFile()->getFilePath()); $constantUseImportTypes = $this->useNodesToAddCollector->getConstantImportsByFilePath( $this->getFile() @@ -56,10 +61,9 @@ public function beforeTraverse(array $nodes): array /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); - $stmts = $rootNode instanceof FileNode ? $rootNode->stmts : $nodes; if ($this->processStmtsWithImportedUses( - $stmts, + $fileNode->getUsedImports(), $useImportTypes, $constantUseImportTypes, $functionUseImportTypes, @@ -85,36 +89,26 @@ public function enterNode(Node $node): int } /** - * @param Stmt[] $stmts * @param FullyQualifiedObjectType[] $useImportTypes * @param FullyQualifiedObjectType[] $constantUseImportTypes * @param FullyQualifiedObjectType[] $functionUseImportTypes */ private function processStmtsWithImportedUses( - array $stmts, + UsedImports $usedImports, array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes, - FileNode|Namespace_ $namespace + FileNode|Namespace_ $node ): bool { - // A. has namespace? add under it - if ($namespace instanceof Namespace_) { - // then add, to prevent adding + removing false positive of same short use - return $this->useImportsAdder->addImportsToNamespace( - $namespace, - $useImportTypes, - $constantUseImportTypes, - $functionUseImportTypes - ); + // no namespace? add in the top, only namespaced names + if ($node instanceof FileNode) { + $useImportTypes = $this->filterOutNonNamespacedNames($useImportTypes); } - // B. no namespace? add in the top - $useImportTypes = $this->filterOutNonNamespacedNames($useImportTypes); - // then add, to prevent adding + removing false positive of same short use return $this->useImportsAdder->addImportsToStmts( - $namespace, - $stmts, + $node, + $usedImports, $useImportTypes, $constantUseImportTypes, $functionUseImportTypes From ccd043eb827a34552312556c0e0d2f9e8fdb98e0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 16 Jun 2026 22:32:58 +0200 Subject: [PATCH 2/8] remove adder, hadnle in FileNode --- .../Application/UseImportsAdder.php | 225 --------------- src/Application/FileProcessor.php | 29 +- src/PhpParser/AstResolver.php | 2 +- src/PhpParser/Node/FileNode.php | 268 +++++++++++++++++- src/PostRector/Rector/UseAddingPostRector.php | 99 +------ src/Testing/TestingParser/TestingParser.php | 11 +- 6 files changed, 288 insertions(+), 346 deletions(-) delete mode 100644 rules/CodingStyle/Application/UseImportsAdder.php diff --git a/rules/CodingStyle/Application/UseImportsAdder.php b/rules/CodingStyle/Application/UseImportsAdder.php deleted file mode 100644 index 6ad7d88a847..00000000000 --- a/rules/CodingStyle/Application/UseImportsAdder.php +++ /dev/null @@ -1,225 +0,0 @@ - $useImportTypes - * @param array $constantUseImportTypes - * @param array $functionUseImportTypes - */ - public function addImportsToStmts( - FileNode|Namespace_ $node, - UsedImports $usedImports, - array $useImportTypes, - array $constantUseImportTypes, - array $functionUseImportTypes - ): bool { - $namespaceName = $node instanceof Namespace_ ? $this->getNamespaceName($node) : null; - - $existingUseImportTypes = $this->typeFactory->uniquateTypes($usedImports->getUseImports()); - - $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); - - $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes( - $constantUseImportTypes, - $usedImports->getConstantImports() - ); - - $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes( - $functionUseImportTypes, - $usedImports->getFunctionImports() - ); - - $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $namespaceName); - if ($newUses === []) { - return false; - } - - // remove empty use stmts - $node->stmts = array_values(array_filter($node->stmts, static function (Stmt $stmt): bool { - if (! $stmt instanceof Use_) { - return true; - } - - return $stmt->uses !== []; - })); - - // place after declare strict_types - foreach ($node->stmts as $key => $stmt) { - // maybe just added a space - if ($stmt instanceof Nop) { - continue; - } - - // when we found a non-declare, directly stop - if (! $stmt instanceof Declare_) { - break; - } - - $nodesToAdd = array_merge([new Nop()], $newUses); - - $this->mirrorUseComments($node->stmts, $newUses, $key + 1); - - // remove space before next use tweak - if (isset($node->stmts[$key + 1]) && ($node->stmts[$key + 1] instanceof Use_ || $node->stmts[$key + 1] instanceof GroupUse)) { - $node->stmts[$key + 1]->setAttribute(AttributeKey::ORIGINAL_NODE, null); - } - - array_splice($node->stmts, $key + 1, 0, $nodesToAdd); - - $node->stmts = array_values($node->stmts); - - return true; - } - - $this->mirrorUseComments($node->stmts, $newUses); - - // make use stmts first - $node->stmts = array_merge($newUses, $this->resolveInsertNop($node), $node->stmts); - $node->stmts = array_values($node->stmts); - - return true; - } - - /** - * @return Nop[] - */ - private function resolveInsertNop(FileNode|Namespace_ $namespace): array - { - $currentStmt = $namespace->stmts[0] ?? null; - if (! $currentStmt instanceof Stmt || $currentStmt instanceof Use_ || $currentStmt instanceof GroupUse) { - return []; - } - - return [new Nop()]; - } - - /** - * @param Stmt[] $stmts - * @param Use_[] $newUses - */ - private function mirrorUseComments(array $stmts, array $newUses, int $indexStmt = 0): void - { - if ($stmts === []) { - return; - } - - if (isset($stmts[$indexStmt]) && $stmts[$indexStmt] instanceof Use_) { - $comments = (array) $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS); - - if ($comments !== []) { - $newUses[0]->setAttribute( - AttributeKey::COMMENTS, - $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS) - ); - - $stmts[$indexStmt]->setAttribute(AttributeKey::COMMENTS, []); - } - } - } - - /** - * @param array $mainTypes - * @param array $typesToRemove - * @return array - */ - private function diffFullyQualifiedObjectTypes(array $mainTypes, array $typesToRemove): array - { - foreach ($mainTypes as $key => $mainType) { - foreach ($typesToRemove as $typeToRemove) { - if ($mainType->equals($typeToRemove)) { - unset($mainTypes[$key]); - } - } - } - - return array_values($mainTypes); - } - - /** - * @param array $useImportTypes - * @param array $constantUseImportTypes - * @param array $functionUseImportTypes - * @return Use_[] - */ - private function createUses( - array $useImportTypes, - array $constantUseImportTypes, - array $functionUseImportTypes, - ?string $namespaceName - ): array { - $newUses = []; - - /** @var array> $importsMapping */ - $importsMapping = [ - Use_::TYPE_NORMAL => $useImportTypes, - Use_::TYPE_CONSTANT => $constantUseImportTypes, - Use_::TYPE_FUNCTION => $functionUseImportTypes, - ]; - - foreach ($importsMapping as $type => $importTypes) { - /** @var AliasedObjectType|FullyQualifiedObjectType $importType */ - foreach ($importTypes as $importType) { - if ($namespaceName !== null && $this->isCurrentNamespace($namespaceName, $importType)) { - continue; - } - - if ($namespaceName === null - && $importType instanceof FullyQualifiedObjectType - && substr_count(ltrim($importType->getClassName(), '\\'), '\\') === 0) { - continue; - } - - // already imported in previous cycle - $newUses[] = $importType->getUseNode($type); - } - } - - return $newUses; - } - - private function getNamespaceName(Namespace_ $namespace): ?string - { - if (! $namespace->name instanceof Name) { - return null; - } - - return $namespace->name->toString(); - } - - private function isCurrentNamespace( - string $namespaceName, - AliasedObjectType|FullyQualifiedObjectType $objectType - ): bool { - $className = $objectType->getClassName(); - - if (! str_starts_with($className, $namespaceName . '\\')) { - return false; - } - - return $namespaceName . '\\' . $objectType->getShortName() === $className; - } -} diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 39fbbf4d7ae..cac5fb8bd75 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -5,6 +5,8 @@ namespace Rector\Application; use Nette\Utils\FileSystem; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use PHPStan\AnalysedCodeException; use PHPStan\Parser\ParserErrorsException; use Rector\Caching\Detector\ChangedFilesDetector; @@ -63,20 +65,13 @@ public function processFile(File $file, Configuration $configuration): FileProce // 1. change nodes with Rector Rules $newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts()); - // 2. refresh used imports on the FileNode from current stmts, - // so post rectors read them without re-traversing, yet stay in sync each iteration - $fileNode = $newStmts[0] ?? null; - if ($fileNode instanceof FileNode) { - $fileNode->setUsedImports($this->usedImportsResolver->resolveForStmts($fileNode->stmts)); - } - - // 3. apply post rectors + // 2. apply post rectors $postNewStmts = $this->postFileProcessor->traverse($newStmts, $file); - // 4. this is needed for new tokens added in "afterTraverse()" + // 3. this is needed for new tokens added in "afterTraverse()" $file->changeNewStmts($postNewStmts); - // 5. print to file or string + // 4. print to file or string // important to detect if file has changed $this->printFile($file, $configuration, $filePath); @@ -88,7 +83,7 @@ public function processFile(File $file, Configuration $configuration): FileProce $fileHasChanged = true; } while (true); - // 6. add as cacheable if not changed at all + // 5. add as cacheable if not changed at all if (! $fileHasChanged) { $this->changedFilesDetector->addCacheableFile($filePath); } else { @@ -179,8 +174,16 @@ private function parseFileNodes(File $file, bool $forNewestSupportedVersion = tr $oldStmts = $stmtsAndTokens->getStmts(); - // wrap in FileNode to allow file-level rules - $oldStmts = [new FileNode($oldStmts)]; + // resolve names up front, so used imports (incl. the class FQN) are resolvable at construction, + // before scope decoration runs; only annotates namespacedName, does not replace name nodes + $nameResolvingTraverser = new NodeTraverser(new NameResolver(null, [ + 'preserveOriginalNames' => true, + 'replaceNodes' => false, + ])); + $nameResolvingTraverser->traverse($oldStmts); + + // wrap in FileNode to allow file-level rules; seed used imports once, kept in sync incrementally + $oldStmts = [new FileNode($oldStmts, $this->usedImportsResolver->resolveForStmts($oldStmts))]; $oldTokens = $stmtsAndTokens->getTokens(); diff --git a/src/PhpParser/AstResolver.php b/src/PhpParser/AstResolver.php index 3b1047bde6f..ad67a725a82 100644 --- a/src/PhpParser/AstResolver.php +++ b/src/PhpParser/AstResolver.php @@ -306,7 +306,7 @@ function (Node $node) use ($desiredClassName, $desiredPropertyName, &$propertyNo /** * @return Stmt[] */ - public function parseFileNameToDecoratedNodes(?string $fileName): array + private function parseFileNameToDecoratedNodes(?string $fileName): array { // probably native PHP → un-parseable if ($fileName === null) { diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index 505d257a43a..1dd347faf8b 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -5,28 +5,30 @@ namespace Rector\PhpParser\Node; use PhpParser\Node; +use PhpParser\Node\Name; use PhpParser\Node\Stmt; +use PhpParser\Node\Stmt\Declare_; use PhpParser\Node\Stmt\GroupUse; use PhpParser\Node\Stmt\Namespace_; +use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Use_; use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; -use Rector\Exception\ShouldNotHappenException; +use Rector\NodeTypeResolver\Node\AttributeKey; +use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; +use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; /** * Inspired by https://github.com/phpstan/phpstan-src/commit/ed81c3ad0b9877e6122c79b4afda9d10f3994092 */ class FileNode extends Stmt { - /** - * Resolved once on file parse, so used imports do not have to be re-traversed later - */ - private ?UsedImports $usedImports = null; - /** * @param Stmt[] $stmts + * @param UsedImports $usedImports Resolved once on file parse, then kept in sync incrementally as imports are added */ public function __construct( public array $stmts, + private UsedImports $usedImports, ) { $firstStmt = $stmts[0] ?? null; $attributes = $firstStmt instanceof Node ? $firstStmt->getAttributes() : []; @@ -34,20 +36,100 @@ public function __construct( parent::__construct($attributes); } + /** + * Seeded once right after node decoration, when namespaced names are resolvable + */ public function setUsedImports(UsedImports $usedImports): void { $this->usedImports = $usedImports; } - public function getUsedImports(): UsedImports - { - if (! $this->usedImports instanceof UsedImports) { - throw new ShouldNotHappenException( - 'Used imports are not set yet; call setUsedImports() on file parse first.' - ); + /** + * Adds new use imports into the file/namespace and keeps the tracked used imports in sync, + * so the next traversal iteration converges without re-resolving. + * + * @param array $useImportTypes + * @param FullyQualifiedObjectType[] $constantUseImportTypes + * @param FullyQualifiedObjectType[] $functionUseImportTypes + */ + public function addImports( + array $useImportTypes, + array $constantUseImportTypes, + array $functionUseImportTypes + ): bool { + $namespace = $this->resolvePlacementNamespace(); + $placementNode = $namespace ?? $this; + $namespaceName = $namespace instanceof Namespace_ && $namespace->name instanceof Name + ? $namespace->name->toString() + : null; + + // no namespace? only keep namespaced names at the file top + if (! $namespace instanceof Namespace_) { + $useImportTypes = $this->filterOutNonNamespacedNames($useImportTypes); + } + + $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $this->usedImports->getUseImports()); + $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes( + $constantUseImportTypes, + $this->usedImports->getConstantImports() + ); + $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes( + $functionUseImportTypes, + $this->usedImports->getFunctionImports() + ); + + $newUses = $this->createUses($useImportTypes, $constantUseImportTypes, $functionUseImportTypes, $namespaceName); + if ($newUses === []) { + return false; + } + + // remove empty use stmts + $placementNode->stmts = array_values(array_filter($placementNode->stmts, static function (Stmt $stmt): bool { + if (! $stmt instanceof Use_) { + return true; + } + + return $stmt->uses !== []; + })); + + // place after declare strict_types + foreach ($placementNode->stmts as $key => $stmt) { + // maybe just added a space + if ($stmt instanceof Nop) { + continue; + } + + // when we found a non-declare, directly stop + if (! $stmt instanceof Declare_) { + break; + } + + $nodesToAdd = array_merge([new Nop()], $newUses); + + $this->mirrorUseComments($placementNode->stmts, $newUses, $key + 1); + + // remove space before next use tweak + $nextStmt = $placementNode->stmts[$key + 1] ?? null; + if ($nextStmt instanceof Use_ || $nextStmt instanceof GroupUse) { + $nextStmt->setAttribute(AttributeKey::ORIGINAL_NODE, null); + } + + array_splice($placementNode->stmts, $key + 1, 0, $nodesToAdd); + + $this->appendUsedImports($useImportTypes, $functionUseImportTypes, $constantUseImportTypes); + + return true; } - return $this->usedImports; + $this->mirrorUseComments($placementNode->stmts, $newUses); + + // make use stmts first + $placementNode->stmts = array_merge($newUses, $this->resolveInsertNop($placementNode), $placementNode->stmts); + $placementNode->stmts = array_values($placementNode->stmts); + + $this->appendUsedImports($useImportTypes, $functionUseImportTypes, $constantUseImportTypes); + + return true; } /** @@ -118,4 +200,164 @@ public function getUses(): array return array_filter($rootNode->stmts, static fn (Stmt $stmt): bool => $stmt instanceof Use_); } + + /** + * Mirrors the legacy placement target: the first namespace if any, else the file root itself + */ + private function resolvePlacementNamespace(): ?Namespace_ + { + foreach ($this->stmts as $stmt) { + if ($stmt instanceof Namespace_) { + return $stmt; + } + } + + return null; + } + + /** + * @param array $useImportTypes + * @return array + */ + private function filterOutNonNamespacedNames(array $useImportTypes): array + { + $namespacedUseImportTypes = []; + + foreach ($useImportTypes as $useImportType) { + if (! str_contains($useImportType->getClassName(), '\\')) { + continue; + } + + $namespacedUseImportTypes[] = $useImportType; + } + + return $namespacedUseImportTypes; + } + + /** + * @param array $useImportTypes + * @param FullyQualifiedObjectType[] $functionUseImportTypes + * @param FullyQualifiedObjectType[] $constantUseImportTypes + */ + private function appendUsedImports( + array $useImportTypes, + array $functionUseImportTypes, + array $constantUseImportTypes + ): void { + $this->usedImports = new UsedImports( + array_merge($this->usedImports->getUseImports(), $useImportTypes), + array_merge($this->usedImports->getFunctionImports(), $functionUseImportTypes), + array_merge($this->usedImports->getConstantImports(), $constantUseImportTypes), + ); + } + + /** + * @template TObjectType of FullyQualifiedObjectType|AliasedObjectType + * @param array $mainTypes + * @param array $typesToRemove + * @return list + */ + private function diffFullyQualifiedObjectTypes(array $mainTypes, array $typesToRemove): array + { + foreach ($mainTypes as $key => $mainType) { + foreach ($typesToRemove as $typeToRemove) { + if ($mainType->equals($typeToRemove)) { + unset($mainTypes[$key]); + } + } + } + + return array_values($mainTypes); + } + + /** + * @param array $useImportTypes + * @param array $constantUseImportTypes + * @param array $functionUseImportTypes + * @return Use_[] + */ + private function createUses( + array $useImportTypes, + array $constantUseImportTypes, + array $functionUseImportTypes, + ?string $namespaceName + ): array { + $newUses = []; + + /** @var array> $importsMapping */ + $importsMapping = [ + Use_::TYPE_NORMAL => $useImportTypes, + Use_::TYPE_CONSTANT => $constantUseImportTypes, + Use_::TYPE_FUNCTION => $functionUseImportTypes, + ]; + + foreach ($importsMapping as $type => $importTypes) { + /** @var AliasedObjectType|FullyQualifiedObjectType $importType */ + foreach ($importTypes as $importType) { + if ($namespaceName !== null && $this->isCurrentNamespace($namespaceName, $importType)) { + continue; + } + + if ($namespaceName === null + && $importType instanceof FullyQualifiedObjectType + && substr_count(ltrim($importType->getClassName(), '\\'), '\\') === 0) { + continue; + } + + $newUses[] = $importType->getUseNode($type); + } + } + + return $newUses; + } + + private function isCurrentNamespace( + string $namespaceName, + AliasedObjectType|FullyQualifiedObjectType $objectType + ): bool { + $className = $objectType->getClassName(); + + if (! str_starts_with($className, $namespaceName . '\\')) { + return false; + } + + return $namespaceName . '\\' . $objectType->getShortName() === $className; + } + + /** + * @return Nop[] + */ + private function resolveInsertNop(self|Namespace_ $node): array + { + $currentStmt = $node->stmts[0] ?? null; + if (! $currentStmt instanceof Stmt || $currentStmt instanceof Use_ || $currentStmt instanceof GroupUse) { + return []; + } + + return [new Nop()]; + } + + /** + * @param Stmt[] $stmts + * @param Use_[] $newUses + */ + private function mirrorUseComments(array $stmts, array $newUses, int $indexStmt = 0): void + { + if ($stmts === []) { + return; + } + + if (isset($stmts[$indexStmt]) && $stmts[$indexStmt] instanceof Use_) { + $comments = (array) $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS); + + if ($comments !== []) { + $newUses[0]->setAttribute( + AttributeKey::COMMENTS, + $stmts[$indexStmt]->getAttribute(AttributeKey::COMMENTS) + ); + + $stmts[$indexStmt]->setAttribute(AttributeKey::COMMENTS, []); + } + } + } } diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index 89d895c63b0..f56ff958d94 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -6,10 +6,7 @@ use PhpParser\Node; use PhpParser\Node\Stmt; -use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeVisitor; -use Rector\CodingStyle\Application\UseImportsAdder; -use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; use Rector\PhpParser\Node\FileNode; use Rector\PostRector\Collector\UseNodesToAddCollector; @@ -19,7 +16,6 @@ final class UseAddingPostRector extends AbstractPostRector { public function __construct( private readonly TypeFactory $typeFactory, - private readonly UseImportsAdder $useImportsAdder, private readonly UseNodesToAddCollector $useNodesToAddCollector, ) { } @@ -30,20 +26,11 @@ public function __construct( */ public function beforeTraverse(array $nodes): array { - // no nodes → just return - if ($nodes === []) { + $fileNode = $nodes[0] ?? null; + if (! $fileNode instanceof FileNode) { return $nodes; } - $rootNode = $this->resolveRootNode($nodes); - if (! $rootNode instanceof FileNode && ! $rootNode instanceof Namespace_) { - return $nodes; - } - - // the used imports are resolved once on file parse and stored on the FileNode root - /** @var FileNode $fileNode */ - $fileNode = $nodes[0]; - $useImportTypes = $this->useNodesToAddCollector->getObjectImportsByFilePath($this->getFile()->getFilePath()); $constantUseImportTypes = $this->useNodesToAddCollector->getConstantImportsByFilePath( $this->getFile() @@ -62,14 +49,10 @@ public function beforeTraverse(array $nodes): array /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); - if ($this->processStmtsWithImportedUses( - $fileNode->getUsedImports(), - $useImportTypes, - $constantUseImportTypes, - $functionUseImportTypes, - $rootNode - )) { - $this->addRectorClassWithLine($rootNode); + // the FileNode owns its used imports and keeps them in sync, so the run converges + // without re-resolving on each traversal iteration + if ($fileNode->addImports($useImportTypes, $constantUseImportTypes, $functionUseImportTypes)) { + $this->addRectorClassWithLine($fileNode); } return $nodes; @@ -87,74 +70,4 @@ public function enterNode(Node $node): int */ return NodeVisitor::STOP_TRAVERSAL; } - - /** - * @param FullyQualifiedObjectType[] $useImportTypes - * @param FullyQualifiedObjectType[] $constantUseImportTypes - * @param FullyQualifiedObjectType[] $functionUseImportTypes - */ - private function processStmtsWithImportedUses( - UsedImports $usedImports, - array $useImportTypes, - array $constantUseImportTypes, - array $functionUseImportTypes, - FileNode|Namespace_ $node - ): bool { - // no namespace? add in the top, only namespaced names - if ($node instanceof FileNode) { - $useImportTypes = $this->filterOutNonNamespacedNames($useImportTypes); - } - - // then add, to prevent adding + removing false positive of same short use - return $this->useImportsAdder->addImportsToStmts( - $node, - $usedImports, - $useImportTypes, - $constantUseImportTypes, - $functionUseImportTypes - ); - } - - /** - * Prevents - * @param FullyQualifiedObjectType[] $useImportTypes - * @return FullyQualifiedObjectType[] - */ - private function filterOutNonNamespacedNames(array $useImportTypes): array - { - $namespacedUseImportTypes = []; - - foreach ($useImportTypes as $useImportType) { - if (! \str_contains($useImportType->getClassName(), '\\')) { - continue; - } - - $namespacedUseImportTypes[] = $useImportType; - } - - return $namespacedUseImportTypes; - } - - /** - * @param Stmt[] $nodes - */ - private function resolveRootNode(array $nodes): Namespace_|FileNode|null - { - if ($nodes === []) { - return null; - } - - $firstStmt = $nodes[0]; - if (! $firstStmt instanceof FileNode) { - return null; - } - - foreach ($firstStmt->stmts as $stmt) { - if ($stmt instanceof Namespace_) { - return $stmt; - } - } - - return $firstStmt; - } } diff --git a/src/Testing/TestingParser/TestingParser.php b/src/Testing/TestingParser/TestingParser.php index 5bace90461a..afe7ac49277 100644 --- a/src/Testing/TestingParser/TestingParser.php +++ b/src/Testing/TestingParser/TestingParser.php @@ -7,6 +7,8 @@ use Nette\Utils\FileSystem; use PhpParser\Node; use Rector\Application\Provider\CurrentFileProvider; +use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; +use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider; use Rector\PhpParser\Node\FileNode; @@ -23,6 +25,7 @@ public function __construct( private NodeScopeAndMetadataDecorator $nodeScopeAndMetadataDecorator, private CurrentFileProvider $currentFileProvider, private DynamicSourceLocatorProvider $dynamicSourceLocatorProvider, + private UsedImportsResolver $usedImportsResolver, ) { } @@ -54,9 +57,15 @@ private function parseToFileAndStmts(string $filePath): array $stmts = $this->rectorParser->parseString($fileContent); // wrap in FileNode to enable file-level rules - $stmts = [new FileNode($stmts)]; + $stmts = [new FileNode($stmts, new UsedImports([], [], []))]; $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); + // seed used imports once, after decoration when namespaced names are resolvable + $fileNode = $stmts[0] ?? null; + if ($fileNode instanceof FileNode) { + $fileNode->setUsedImports($this->usedImportsResolver->resolveForStmts($fileNode->stmts)); + } + $file->hydrateStmtsAndTokens($stmts, $stmts, []); $this->currentFileProvider->setFile($file); From cf7a839d6810189810c95e7088197728dc48aa07 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 00:19:53 +0200 Subject: [PATCH 3/8] remove use imrpots remover --- .../Fixture/no_imports.php.inc | 10 ++ .../Fixture/no_namespace.php.inc | 12 ++ .../Fixture/with_imports.php.inc | 17 +++ .../UsedImportsResolverTest.php | 114 ++++++++++++++++++ .../Application/UseImportsRemover.php | 74 ------------ src/PhpParser/Node/FileNode.php | 63 ++++++++-- .../Rector/ClassRenamingPostRector.php | 32 ++--- src/Testing/TestingParser/TestingParser.php | 21 ++-- 8 files changed, 229 insertions(+), 114 deletions(-) create mode 100644 rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/no_imports.php.inc create mode 100644 rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/no_namespace.php.inc create mode 100644 rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/with_imports.php.inc create mode 100644 rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/UsedImportsResolverTest.php delete mode 100644 rules/CodingStyle/Application/UseImportsRemover.php diff --git a/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/no_imports.php.inc b/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/no_imports.php.inc new file mode 100644 index 00000000000..0951d7b2d7a --- /dev/null +++ b/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/Fixture/no_imports.php.inc @@ -0,0 +1,10 @@ +usedImportsResolver = $this->make(UsedImportsResolver::class); + $this->testingParser = $this->make(TestingParser::class); + } + + public function testResolvesUseFunctionAndConstantImports(): void + { + $stmts = $this->testingParser->parseFileToDecoratedNodes(__DIR__ . '/Fixture/with_imports.php.inc'); + + $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); + + // the class itself, the normal use and the aliased use + $useImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getUseImports() + ); + + $this->assertSame([ + 'Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Fixture\WithImports', + 'Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\FirstType', + 'AliasedType', + ], $useImportNames); + + $aliasedType = $usedImports->getUseImports()[2]; + $this->assertInstanceOf(AliasedObjectType::class, $aliasedType); + $this->assertSame( + 'Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\SecondType', + $aliasedType->getFullyQualifiedName() + ); + + $functionImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getFunctionImports() + ); + $this->assertSame( + ['Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\someFunction'], + $functionImportNames + ); + + $constantImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getConstantImports() + ); + $this->assertSame( + ['Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\SOME_CONSTANT'], + $constantImportNames + ); + } + + public function testResolvesClassOnlyWhenNoImports(): void + { + $stmts = $this->testingParser->parseFileToDecoratedNodes(__DIR__ . '/Fixture/no_imports.php.inc'); + + $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); + + $useImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getUseImports() + ); + + $this->assertSame([ + 'Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Fixture\NoImports', + ], $useImportNames); + + $this->assertSame([], $usedImports->getFunctionImports()); + $this->assertSame([], $usedImports->getConstantImports()); + } + + public function testResolvesImportsInNonNamespacedFile(): void + { + $stmts = $this->testingParser->parseFileToDecoratedNodes(__DIR__ . '/Fixture/no_namespace.php.inc'); + + $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); + + $useImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getUseImports() + ); + + $this->assertSame([ + 'NoNamespace', + 'Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\FirstType', + ], $useImportNames); + + $functionImportNames = array_map( + static fn ($objectType): string => $objectType->getClassName(), + $usedImports->getFunctionImports() + ); + $this->assertSame( + ['Rector\Tests\CodingStyle\ClassNameImport\UsedImportsResolver\Source\someFunction'], + $functionImportNames + ); + } +} diff --git a/rules/CodingStyle/Application/UseImportsRemover.php b/rules/CodingStyle/Application/UseImportsRemover.php deleted file mode 100644 index 904cf358e3a..00000000000 --- a/rules/CodingStyle/Application/UseImportsRemover.php +++ /dev/null @@ -1,74 +0,0 @@ -stmts as $key => $stmt) { - if (! $stmt instanceof Use_) { - continue; - } - - if ($this->removeUseFromUse($removedUses, $stmt)) { - $node->stmts[$key] = $stmt; - $hasRemoved = true; - } - - // remove empty uses - if ($stmt->uses === []) { - unset($node->stmts[$key]); - } - } - - if ($hasRemoved) { - $node->stmts = array_values($node->stmts); - } - - return $hasRemoved; - } - - /** - * @param string[] $removedUses - */ - private function removeUseFromUse(array $removedUses, Use_ $use): bool - { - $hasChanged = false; - foreach ($use->uses as $usesKey => $useUse) { - $useName = $useUse->name->toString(); - if (! in_array($useName, $removedUses, true)) { - continue; - } - - if (! $this->renamedNameCollector->has($useName)) { - continue; - } - - unset($use->uses[$usesKey]); - $hasChanged = true; - } - - if ($hasChanged) { - $use->uses = array_values($use->uses); - } - - return $hasChanged; - } -} diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index 1dd347faf8b..53b16a18b69 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -36,14 +36,6 @@ public function __construct( parent::__construct($attributes); } - /** - * Seeded once right after node decoration, when namespaced names are resolvable - */ - public function setUsedImports(UsedImports $usedImports): void - { - $this->usedImports = $usedImports; - } - /** * Adds new use imports into the file/namespace and keeps the tracked used imports in sync, * so the next traversal iteration converges without re-resolving. @@ -132,6 +124,38 @@ public function addImports( return true; } + /** + * Removes the given use imports from the file/namespace + * + * @param string[] $removedUses + */ + public function removeImports(array $removedUses): bool + { + $node = $this->resolvePlacementNamespace() ?? $this; + + $hasRemoved = false; + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof Use_) { + continue; + } + + if ($this->removeUseFromUse($removedUses, $stmt)) { + $hasRemoved = true; + } + + // remove empty uses + if ($stmt->uses === []) { + unset($node->stmts[$key]); + } + } + + if ($hasRemoved) { + $node->stmts = array_values($node->stmts); + } + + return $hasRemoved; + } + /** * This triggers Printed method with "pFileNode" name * @see \Rector\PhpParser\Printer\BetterStandardPrinter::pStmt_FileNode() @@ -360,4 +384,27 @@ private function mirrorUseComments(array $stmts, array $newUses, int $indexStmt } } } + + /** + * @param string[] $removedUses + */ + private function removeUseFromUse(array $removedUses, Use_ $use): bool + { + $hasChanged = false; + foreach ($use->uses as $usesKey => $useUse) { + $useName = $useUse->name->toString(); + if (! in_array($useName, $removedUses, true)) { + continue; + } + + unset($use->uses[$usesKey]); + $hasChanged = true; + } + + if ($hasChanged) { + $use->uses = array_values($use->uses); + } + + return $hasChanged; + } } diff --git a/src/PostRector/Rector/ClassRenamingPostRector.php b/src/PostRector/Rector/ClassRenamingPostRector.php index 51622e7e14d..ec17e2a8524 100644 --- a/src/PostRector/Rector/ClassRenamingPostRector.php +++ b/src/PostRector/Rector/ClassRenamingPostRector.php @@ -6,9 +6,7 @@ use Override; use PhpParser\Node; -use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeVisitor; -use Rector\CodingStyle\Application\UseImportsRemover; use Rector\Configuration\RenamedClassesDataCollector; use Rector\PhpParser\Node\FileNode; use Rector\PostRector\Guard\AddUseStatementGuard; @@ -23,34 +21,22 @@ final class ClassRenamingPostRector extends AbstractPostRector public function __construct( private readonly RenamedClassesDataCollector $renamedClassesDataCollector, - private readonly UseImportsRemover $useImportsRemover, private readonly RenamedNameCollector $renamedNameCollector, private readonly AddUseStatementGuard $addUseStatementGuard, ) { } - public function enterNode(Node $node): Namespace_|FileNode|int|null + public function enterNode(Node $node): FileNode|int { + // the FileNode resolves the namespace-or-file placement internally if ($node instanceof FileNode) { - // handle in Namespace_ node - if ($node->isNamespaced()) { - return null; - } - - // handle here - $removedUses = $this->renamedClassesDataCollector->getOldClasses(); - if ($this->useImportsRemover->removeImportsFromStmts($node, $removedUses)) { - $this->addRectorClassWithLine($node); - } - - $this->renamedNameCollector->reset(); - - return $node; - } + // keep only the uses that were actually renamed + $removedUses = array_values(array_filter( + $this->renamedClassesDataCollector->getOldClasses(), + fn (string $removedUse): bool => $this->renamedNameCollector->has($removedUse) + )); - if ($node instanceof Namespace_) { - $removedUses = $this->renamedClassesDataCollector->getOldClasses(); - if ($this->useImportsRemover->removeImportsFromStmts($node, $removedUses)) { + if ($node->removeImports($removedUses)) { $this->addRectorClassWithLine($node); } @@ -59,7 +45,7 @@ public function enterNode(Node $node): Namespace_|FileNode|int|null return $node; } - // nothing else to handle here, as first 2 nodes we'll hit are handled above + // nothing else to handle here, as the first node we'll hit is handled above return NodeVisitor::STOP_TRAVERSAL; } diff --git a/src/Testing/TestingParser/TestingParser.php b/src/Testing/TestingParser/TestingParser.php index afe7ac49277..d85091217d4 100644 --- a/src/Testing/TestingParser/TestingParser.php +++ b/src/Testing/TestingParser/TestingParser.php @@ -6,9 +6,10 @@ use Nette\Utils\FileSystem; use PhpParser\Node; +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor\NameResolver; use Rector\Application\Provider\CurrentFileProvider; use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; -use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider; use Rector\PhpParser\Node\FileNode; @@ -56,15 +57,17 @@ private function parseToFileAndStmts(string $filePath): array $file = new File($filePath, $fileContent); $stmts = $this->rectorParser->parseString($fileContent); - // wrap in FileNode to enable file-level rules - $stmts = [new FileNode($stmts, new UsedImports([], [], []))]; - $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); + // resolve names up front, so used imports are resolvable at construction, before decoration; + // only annotates namespacedName, does not replace name nodes + $nameResolvingTraverser = new NodeTraverser(new NameResolver(null, [ + 'preserveOriginalNames' => true, + 'replaceNodes' => false, + ])); + $stmts = $nameResolvingTraverser->traverse($stmts); - // seed used imports once, after decoration when namespaced names are resolvable - $fileNode = $stmts[0] ?? null; - if ($fileNode instanceof FileNode) { - $fileNode->setUsedImports($this->usedImportsResolver->resolveForStmts($fileNode->stmts)); - } + // wrap in FileNode to enable file-level rules; seed used imports once, kept in sync incrementally + $stmts = [new FileNode($stmts, $this->usedImportsResolver->resolveForStmts($stmts))]; + $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); $file->hydrateStmtsAndTokens($stmts, $stmts, []); $this->currentFileProvider->setFile($file); From 999d642e09787459fcc59fcef32e5f133ea0f4a4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 11:02:05 +0200 Subject: [PATCH 4/8] instead of service collector, use FileNode directly as source of truth for imports --- .../UsesClassNameImportSkipVoter.php | 14 +- rules/CodingStyle/Node/NameImporter.php | 24 ++- .../RemoveNullArgOnNullDefaultParamRector.php | 6 + .../NestedAnnotationToAttributeRector.php | 18 +- .../NameImportingPhpDocNodeVisitor.php | 20 +- src/PhpParser/Node/FileNode.php | 142 +++++++++++++ .../Collector/UseNodesToAddCollector.php | 193 ------------------ src/PostRector/Rector/UseAddingPostRector.php | 15 +- 8 files changed, 199 insertions(+), 233 deletions(-) delete mode 100644 src/PostRector/Collector/UseNodesToAddCollector.php diff --git a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php index 5c20de92ddd..a397a837651 100644 --- a/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php +++ b/rules/CodingStyle/ClassNameImport/ClassNameImportSkipVoter/UsesClassNameImportSkipVoter.php @@ -6,7 +6,7 @@ use PhpParser\Node; use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface; -use Rector\PostRector\Collector\UseNodesToAddCollector; +use Rector\PhpParser\Node\FileNode; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; use Rector\ValueObject\Application\File; @@ -19,14 +19,14 @@ */ final readonly class UsesClassNameImportSkipVoter implements ClassNameImportSkipVoterInterface { - public function __construct( - private UseNodesToAddCollector $useNodesToAddCollector - ) { - } - public function shouldSkip(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType, Node $node): bool { - $useImportTypes = $this->useNodesToAddCollector->getUseImportTypesByNode($file); + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return false; + } + + $useImportTypes = $fileNode->resolveUsedImportTypes(); foreach ($useImportTypes as $useImportType) { if (! $useImportType->equals($fullyQualifiedObjectType) && $useImportType->areShortNamesEqual( diff --git a/rules/CodingStyle/Node/NameImporter.php b/rules/CodingStyle/Node/NameImporter.php index 404bec35ef3..b8eceaef4d4 100644 --- a/rules/CodingStyle/Node/NameImporter.php +++ b/rules/CodingStyle/Node/NameImporter.php @@ -12,7 +12,7 @@ use Rector\CodingStyle\ClassNameImport\ClassNameImportSkipper; use Rector\Naming\Naming\AliasNameResolver; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PostRector\Collector\UseNodesToAddCollector; +use Rector\PhpParser\Node\FileNode; use Rector\StaticTypeMapper\PhpParser\FullyQualifiedNodeMapper; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; use Rector\ValueObject\Application\File; @@ -22,7 +22,6 @@ public function __construct( private ClassNameImportSkipper $classNameImportSkipper, private FullyQualifiedNodeMapper $fullyQualifiedNodeMapper, - private UseNodesToAddCollector $useNodesToAddCollector, private AliasNameResolver $aliasNameResolver ) { } @@ -100,15 +99,20 @@ private function importNameAndCollectNewUseStatement( return null; } - if ($this->useNodesToAddCollector->isShortImported($file, $fullyQualifiedObjectType)) { - if ($this->useNodesToAddCollector->isImportShortable($file, $fullyQualifiedObjectType)) { + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return null; + } + + if ($fileNode->isShortImported($fullyQualifiedObjectType)) { + if ($fileNode->isImportShortable($fullyQualifiedObjectType)) { return $fullyQualifiedObjectType->getShortNameNode(); } return null; } - $this->addUseImport($file, $fullyQualified, $fullyQualifiedObjectType); + $this->addUseImport($fileNode, $fullyQualified, $fullyQualifiedObjectType); $name = $fullyQualifiedObjectType->getShortNameNode(); $oldTokens = $file->getOldTokens(); @@ -135,20 +139,20 @@ private function importNameAndCollectNewUseStatement( } private function addUseImport( - File $file, + FileNode $fileNode, FullyQualified $fullyQualified, FullyQualifiedObjectType $fullyQualifiedObjectType ): void { - if ($this->useNodesToAddCollector->hasImport($file, $fullyQualifiedObjectType)) { + if ($fileNode->hasImport($fullyQualifiedObjectType)) { return; } if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === true) { - $this->useNodesToAddCollector->addFunctionUseImport($fullyQualifiedObjectType); + $fileNode->addFunctionUseImport($fullyQualifiedObjectType); } elseif ($fullyQualified->getAttribute(AttributeKey::IS_CONSTFETCH_NAME) === true) { - $this->useNodesToAddCollector->addConstantUseImport($fullyQualifiedObjectType); + $fileNode->addConstantUseImport($fullyQualifiedObjectType); } else { - $this->useNodesToAddCollector->addUseImport($fullyQualifiedObjectType); + $fileNode->addUseImport($fullyQualifiedObjectType); } } } diff --git a/rules/DeadCode/Rector/MethodCall/RemoveNullArgOnNullDefaultParamRector.php b/rules/DeadCode/Rector/MethodCall/RemoveNullArgOnNullDefaultParamRector.php index 248c01197cb..855e3ffde7d 100644 --- a/rules/DeadCode/Rector/MethodCall/RemoveNullArgOnNullDefaultParamRector.php +++ b/rules/DeadCode/Rector/MethodCall/RemoveNullArgOnNullDefaultParamRector.php @@ -110,6 +110,12 @@ public function refactor(Node $node): StaticCall|MethodCall|New_|FuncCall|null $arg = $args[$position]; if (! $this->valueResolver->isNull($arg->value)) { + // a named non-null argument can be skipped over: removing an earlier + // named null argument still leaves the remaining named arguments validly bound + if ($arg->name instanceof Identifier) { + continue; + } + break; } diff --git a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php index ab105675ea6..d73ce17cd8d 100644 --- a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php @@ -11,6 +11,7 @@ use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Use_; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; +use Rector\Application\Provider\CurrentFileProvider; use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; @@ -24,9 +25,10 @@ use Rector\Php80\ValueObject\AnnotationPropertyToAttributeClass; use Rector\Php80\ValueObject\NestedAnnotationToAttribute; use Rector\Php80\ValueObject\NestedDoctrineTagAndAnnotationToAttribute; -use Rector\PostRector\Collector\UseNodesToAddCollector; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; +use Rector\ValueObject\Application\File; use Rector\ValueObject\PhpVersion; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; @@ -47,7 +49,7 @@ public function __construct( private readonly UseImportsResolver $useImportsResolver, private readonly PhpDocTagRemover $phpDocTagRemover, private readonly NestedAttrGroupsFactory $nestedAttrGroupsFactory, - private readonly UseNodesToAddCollector $useNodesToAddCollector, + private readonly CurrentFileProvider $currentFileProvider, private readonly DocBlockUpdater $docBlockUpdater, private readonly PhpDocInfoFactory $phpDocInfoFactory, ) { @@ -212,6 +214,16 @@ private function matchAnnotationToAttribute( */ private function completeExtraUseImports(array $attributeGroups): void { + $file = $this->currentFileProvider->getFile(); + if (! $file instanceof File) { + return; + } + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return; + } + foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attr) { $namespacedAttrName = $attr->name->getAttribute(AttributeKey::EXTRA_USE_IMPORT); @@ -219,7 +231,7 @@ private function completeExtraUseImports(array $attributeGroups): void continue; } - $this->useNodesToAddCollector->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); + $fileNode->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); } } } diff --git a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php index dc1006472ee..0e0e50f39e9 100644 --- a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php +++ b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php @@ -18,7 +18,7 @@ use Rector\CodingStyle\ClassNameImport\ClassNameImportSkipper; use Rector\Exception\ShouldNotHappenException; use Rector\PhpDocParser\PhpDocParser\PhpDocNodeVisitor\AbstractPhpDocNodeVisitor; -use Rector\PostRector\Collector\UseNodesToAddCollector; +use Rector\PhpParser\Node\FileNode; use Rector\StaticTypeMapper\PhpDocParser\IdentifierPhpDocTypeMapper; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; @@ -33,7 +33,6 @@ final class NameImportingPhpDocNodeVisitor extends AbstractPhpDocNodeVisitor public function __construct( private readonly ClassNameImportSkipper $classNameImportSkipper, - private readonly UseNodesToAddCollector $useNodesToAddCollector, private readonly CurrentFileProvider $currentFileProvider, private readonly ReflectionProvider $reflectionProvider, private readonly IdentifierPhpDocTypeMapper $identifierPhpDocTypeMapper @@ -139,14 +138,19 @@ private function processFqnNameImport( $newNode = new IdentifierTypeNode($fullyQualifiedObjectType->getShortName()); + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return null; + } + // should skip because its already used - if ($this->useNodesToAddCollector->isShortImported($file, $fullyQualifiedObjectType) - && ! $this->useNodesToAddCollector->isImportShortable($file, $fullyQualifiedObjectType)) { + if ($fileNode->isShortImported($fullyQualifiedObjectType) + && ! $fileNode->isImportShortable($fullyQualifiedObjectType)) { return null; } - if ($this->shouldImport($file, $newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { - $this->useNodesToAddCollector->addUseImport($fullyQualifiedObjectType); + if ($this->shouldImport($fileNode, $newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { + $fileNode->addUseImport($fullyQualifiedObjectType); $this->hasChanged = true; return $newNode; @@ -156,7 +160,7 @@ private function processFqnNameImport( } private function shouldImport( - File $file, + FileNode $fileNode, IdentifierTypeNode $newNode, IdentifierTypeNode $identifierTypeNode, FullyQualifiedObjectType $fullyQualifiedObjectType @@ -181,7 +185,7 @@ private function shouldImport( $firstPath = Strings::before($identifierTypeNode->name, '\\' . $newNode->name); if ($firstPath === null) { - return ! $this->useNodesToAddCollector->hasImport($file, $fullyQualifiedObjectType); + return ! $fileNode->hasImport($fullyQualifiedObjectType); } if ($firstPath === '') { diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index 53b16a18b69..3823546bec8 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -5,6 +5,7 @@ namespace Rector\PhpParser\Node; use PhpParser\Node; +use PhpParser\Node\Identifier; use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Declare_; @@ -22,6 +23,22 @@ */ class FileNode extends Stmt { + /** + * Imports queued to be added on the next UseAddingPostRector run; scoped to this file + * @var FullyQualifiedObjectType[] + */ + private array $pendingUseImports = []; + + /** + * @var FullyQualifiedObjectType[] + */ + private array $pendingFunctionImports = []; + + /** + * @var FullyQualifiedObjectType[] + */ + private array $pendingConstantImports = []; + /** * @param Stmt[] $stmts * @param UsedImports $usedImports Resolved once on file parse, then kept in sync incrementally as imports are added @@ -124,6 +141,131 @@ public function addImports( return true; } + public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->pendingUseImports[] = $fullyQualifiedObjectType; + } + + public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->pendingFunctionImports[] = $fullyQualifiedObjectType; + } + + public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->pendingConstantImports[] = $fullyQualifiedObjectType; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getPendingUseImports(): array + { + return $this->pendingUseImports; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getPendingFunctionImports(): array + { + return $this->pendingFunctionImports; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getPendingConstantImports(): array + { + return $this->pendingConstantImports; + } + + public function hasImport(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + foreach ($this->resolveUsedImportTypes() as $useImport) { + if ($useImport->equals($fullyQualifiedObjectType)) { + return true; + } + } + + return false; + } + + public function isShortImported(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $shortName = $fullyQualifiedObjectType->getShortName(); + + foreach ($this->pendingConstantImports as $pendingConstantImport) { + // don't compare strtolower for use const as insensitive is allowed, see https://3v4l.org/lteVa + if ($pendingConstantImport->getShortName() === $shortName) { + return true; + } + } + + $shortName = strtolower($shortName); + + foreach ($this->pendingUseImports as $pendingUseImport) { + if (strtolower($pendingUseImport->getShortName()) === $shortName) { + return true; + } + } + + foreach ($this->pendingFunctionImports as $pendingFunctionImport) { + if (strtolower($pendingFunctionImport->getShortName()) === $shortName) { + return true; + } + } + + return false; + } + + public function isImportShortable(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + foreach ($this->pendingUseImports as $pendingUseImport) { + if ($fullyQualifiedObjectType->equals($pendingUseImport)) { + return true; + } + } + + foreach ($this->pendingConstantImports as $pendingConstantImport) { + if ($fullyQualifiedObjectType->equals($pendingConstantImport)) { + return true; + } + } + + foreach ($this->pendingFunctionImports as $pendingFunctionImport) { + if ($fullyQualifiedObjectType->equals($pendingFunctionImport)) { + return true; + } + } + + return false; + } + + /** + * The queued use imports merged with the use imports already present in the file + * + * @return array + */ + public function resolveUsedImportTypes(): array + { + $objectTypes = $this->pendingUseImports; + + foreach ($this->getUsesAndGroupUses() as $use) { + $prefix = $use instanceof GroupUse ? $use->prefix . '\\' : ''; + + foreach ($use->uses as $useUse) { + if ($useUse->alias instanceof Identifier) { + $objectTypes[] = new AliasedObjectType($useUse->alias->toString(), $prefix . $useUse->name); + } else { + $objectTypes[] = new FullyQualifiedObjectType($prefix . $useUse->name); + } + } + } + + return $objectTypes; + } + /** * Removes the given use imports from the file/namespace * diff --git a/src/PostRector/Collector/UseNodesToAddCollector.php b/src/PostRector/Collector/UseNodesToAddCollector.php deleted file mode 100644 index ebc27a8357e..00000000000 --- a/src/PostRector/Collector/UseNodesToAddCollector.php +++ /dev/null @@ -1,193 +0,0 @@ - - */ - private array $constantUseImportTypesInFilePath = []; - - /** - * @var array - */ - private array $functionUseImportTypesInFilePath = []; - - /** - * @var array - */ - private array $useImportTypesInFilePath = []; - - public function __construct( - private readonly CurrentFileProvider $currentFileProvider, - private readonly UseImportsResolver $useImportsResolver, - ) { - } - - public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - // @todo consider using FileNode directly - /** @var File $file */ - $file = $this->currentFileProvider->getFile(); - - $this->useImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; - } - - public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - /** @var File $file */ - $file = $this->currentFileProvider->getFile(); - - $this->constantUseImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; - } - - public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - /** @var File $file */ - $file = $this->currentFileProvider->getFile(); - - $this->functionUseImportTypesInFilePath[$file->getFilePath()][] = $fullyQualifiedObjectType; - } - - /** - * @return AliasedObjectType[]|FullyQualifiedObjectType[] - */ - public function getUseImportTypesByNode(File $file): array - { - $filePath = $file->getFilePath(); - $objectTypes = $this->useImportTypesInFilePath[$filePath] ?? []; - - $uses = $this->useImportsResolver->resolve(); - - foreach ($uses as $use) { - $prefix = $this->useImportsResolver->resolvePrefix($use); - - foreach ($use->uses as $useUse) { - if ($useUse->alias instanceof Identifier) { - $objectTypes[] = new AliasedObjectType($useUse->alias->toString(), $prefix . $useUse->name); - } else { - $objectTypes[] = new FullyQualifiedObjectType($prefix . $useUse->name); - } - } - } - - return $objectTypes; - } - - public function hasImport(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool - { - $useImports = $this->getUseImportTypesByNode($file); - - foreach ($useImports as $useImport) { - if ($useImport->equals($fullyQualifiedObjectType)) { - return true; - } - } - - return false; - } - - public function isShortImported(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool - { - $shortName = $fullyQualifiedObjectType->getShortName(); - $filePath = $file->getFilePath(); - - $fileConstantUseImportTypes = $this->constantUseImportTypesInFilePath[$filePath] ?? []; - - foreach ($fileConstantUseImportTypes as $fileConstantUseImportType) { - // don't compare strtolower for use const as insensitive is allowed, see https://3v4l.org/lteVa - if ($fileConstantUseImportType->getShortName() === $shortName) { - return true; - } - } - - $shortName = strtolower($shortName); - if ($this->isShortClassImported($filePath, $shortName)) { - return true; - } - - $fileFunctionUseImportTypes = $this->functionUseImportTypesInFilePath[$filePath] ?? []; - foreach ($fileFunctionUseImportTypes as $fileFunctionUseImportType) { - if (strtolower($fileFunctionUseImportType->getShortName()) === $shortName) { - return true; - } - } - - return false; - } - - public function isImportShortable(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool - { - $filePath = $file->getFilePath(); - $fileUseImportTypes = $this->useImportTypesInFilePath[$filePath] ?? []; - - foreach ($fileUseImportTypes as $fileUseImportType) { - if ($fullyQualifiedObjectType->equals($fileUseImportType)) { - return true; - } - } - - $constantImports = $this->constantUseImportTypesInFilePath[$filePath] ?? []; - foreach ($constantImports as $constantImport) { - if ($fullyQualifiedObjectType->equals($constantImport)) { - return true; - } - } - - $functionImports = $this->functionUseImportTypesInFilePath[$filePath] ?? []; - foreach ($functionImports as $functionImport) { - if ($fullyQualifiedObjectType->equals($functionImport)) { - return true; - } - } - - return false; - } - - /** - * @return AliasedObjectType[]|FullyQualifiedObjectType[] - */ - public function getObjectImportsByFilePath(string $filePath): array - { - return $this->useImportTypesInFilePath[$filePath] ?? []; - } - - /** - * @return FullyQualifiedObjectType[] - */ - public function getConstantImportsByFilePath(string $filePath): array - { - return $this->constantUseImportTypesInFilePath[$filePath] ?? []; - } - - /** - * @return FullyQualifiedObjectType[] - */ - public function getFunctionImportsByFilePath(string $filePath): array - { - return $this->functionUseImportTypesInFilePath[$filePath] ?? []; - } - - private function isShortClassImported(string $filePath, string $shortName): bool - { - $fileUseImports = $this->useImportTypesInFilePath[$filePath] ?? []; - - foreach ($fileUseImports as $fileUseImport) { - if (strtolower($fileUseImport->getShortName()) === $shortName) { - return true; - } - } - - return false; - } -} diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index f56ff958d94..3616bd869a5 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -9,14 +9,12 @@ use PhpParser\NodeVisitor; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; use Rector\PhpParser\Node\FileNode; -use Rector\PostRector\Collector\UseNodesToAddCollector; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; final class UseAddingPostRector extends AbstractPostRector { public function __construct( private readonly TypeFactory $typeFactory, - private readonly UseNodesToAddCollector $useNodesToAddCollector, ) { } @@ -31,16 +29,9 @@ public function beforeTraverse(array $nodes): array return $nodes; } - $useImportTypes = $this->useNodesToAddCollector->getObjectImportsByFilePath($this->getFile()->getFilePath()); - $constantUseImportTypes = $this->useNodesToAddCollector->getConstantImportsByFilePath( - $this->getFile() - ->getFilePath() - ); - - $functionUseImportTypes = $this->useNodesToAddCollector->getFunctionImportsByFilePath( - $this->getFile() - ->getFilePath() - ); + $useImportTypes = $fileNode->getPendingUseImports(); + $constantUseImportTypes = $fileNode->getPendingConstantImports(); + $functionUseImportTypes = $fileNode->getPendingFunctionImports(); if ($useImportTypes === [] && $constantUseImportTypes === [] && $functionUseImportTypes === []) { return $nodes; From 03480b19cde3071fe0e563c1d4325901686829e5 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 12:03:00 +0200 Subject: [PATCH 5/8] remove currentFileProvider from NestedAnnotationToAttributeRector as internal --- .../ClassNameImport/UsedImportsResolver.php | 13 ++++++------ .../NestedAnnotationToAttributeRector.php | 10 +-------- src/Application/FileProcessor.php | 3 ++- src/PhpParser/Node/FileNode.php | 13 ++++++++++++ src/PostRector/Rector/UseAddingPostRector.php | 21 ++++++++++++------- 5 files changed, 37 insertions(+), 23 deletions(-) diff --git a/rules/CodingStyle/ClassNameImport/UsedImportsResolver.php b/rules/CodingStyle/ClassNameImport/UsedImportsResolver.php index 5e8d84bdc91..5dce3f8fb27 100644 --- a/rules/CodingStyle/ClassNameImport/UsedImportsResolver.php +++ b/rules/CodingStyle/ClassNameImport/UsedImportsResolver.php @@ -29,7 +29,7 @@ public function __construct( */ public function resolveForStmts(array $stmts): UsedImports { - $usedImports = []; + $useImports = []; /** @var Class_|null $class */ $class = $this->betterNodeFinder->findFirstInstanceOf($stmts, Class_::class); @@ -38,22 +38,23 @@ public function resolveForStmts(array $stmts): UsedImports // is not anonymous class if ($class instanceof Class_) { $className = (string) $this->nodeNameResolver->getName($class); - $usedImports[] = new FullyQualifiedObjectType($className); + $useImports[] = new FullyQualifiedObjectType($className); } $usedConstImports = []; $usedFunctionImports = []; + /** @param Use_::TYPE_* $useType */ $this->useImportsTraverser->traverserStmts($stmts, static function ( int $useType, UseItem $useItem, string $name - ) use (&$usedImports, &$usedFunctionImports, &$usedConstImports): void { + ) use (&$useImports, &$usedFunctionImports, &$usedConstImports): void { if ($useType === Use_::TYPE_NORMAL) { if ($useItem->alias instanceof Identifier) { - $usedImports[] = new AliasedObjectType($useItem->alias->toString(), $name); + $useImports[] = new AliasedObjectType($useItem->alias->toString(), $name); } else { - $usedImports[] = new FullyQualifiedObjectType($name); + $useImports[] = new FullyQualifiedObjectType($name); } } @@ -66,6 +67,6 @@ public function resolveForStmts(array $stmts): UsedImports } }); - return new UsedImports($usedImports, $usedFunctionImports, $usedConstImports); + return new UsedImports($useImports, $usedFunctionImports, $usedConstImports); } } diff --git a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php index d73ce17cd8d..c1e565b09c6 100644 --- a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php @@ -11,7 +11,6 @@ use PhpParser\Node\Stmt\Property; use PhpParser\Node\Stmt\Use_; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; -use Rector\Application\Provider\CurrentFileProvider; use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; @@ -28,7 +27,6 @@ use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; -use Rector\ValueObject\Application\File; use Rector\ValueObject\PhpVersion; use Rector\VersionBonding\Contract\MinPhpVersionInterface; use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample; @@ -49,7 +47,6 @@ public function __construct( private readonly UseImportsResolver $useImportsResolver, private readonly PhpDocTagRemover $phpDocTagRemover, private readonly NestedAttrGroupsFactory $nestedAttrGroupsFactory, - private readonly CurrentFileProvider $currentFileProvider, private readonly DocBlockUpdater $docBlockUpdater, private readonly PhpDocInfoFactory $phpDocInfoFactory, ) { @@ -214,12 +211,7 @@ private function matchAnnotationToAttribute( */ private function completeExtraUseImports(array $attributeGroups): void { - $file = $this->currentFileProvider->getFile(); - if (! $file instanceof File) { - return; - } - - $fileNode = $file->getFileNode(); + $fileNode = $this->file->getFileNode(); if (! $fileNode instanceof FileNode) { return; } diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index cac5fb8bd75..8eeaf3ec03b 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -183,7 +183,8 @@ private function parseFileNodes(File $file, bool $forNewestSupportedVersion = tr $nameResolvingTraverser->traverse($oldStmts); // wrap in FileNode to allow file-level rules; seed used imports once, kept in sync incrementally - $oldStmts = [new FileNode($oldStmts, $this->usedImportsResolver->resolveForStmts($oldStmts))]; + $usedImports = $this->usedImportsResolver->resolveForStmts($oldStmts); + $oldStmts = [new FileNode($oldStmts, $usedImports)]; $oldTokens = $stmtsAndTokens->getTokens(); diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index 3823546bec8..187270376c9 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -156,6 +156,19 @@ public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObj $this->pendingConstantImports[] = $fullyQualifiedObjectType; } + public function hasPendingUseImports(): bool + { + if ($this->pendingUseImports !== []) { + return true; + } + + if ($this->pendingFunctionImports !== []) { + return true; + } + + return $this->pendingConstantImports !== []; + } + /** * @return FullyQualifiedObjectType[] */ diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index 3616bd869a5..dcedced4622 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -18,25 +18,32 @@ public function __construct( ) { } + /** + * @param Stmt[] $stmts + */ + public function shouldTraverse(array $stmts): bool + { + $fileNode = $stmts[0] ?? null; + if (! $fileNode instanceof FileNode) { + return false; + } + + return $fileNode->hasPendingUseImports(); + } + /** * @param Stmt[] $nodes * @return Stmt[] */ public function beforeTraverse(array $nodes): array { + /** @var FileNode $fileNode */ $fileNode = $nodes[0] ?? null; - if (! $fileNode instanceof FileNode) { - return $nodes; - } $useImportTypes = $fileNode->getPendingUseImports(); $constantUseImportTypes = $fileNode->getPendingConstantImports(); $functionUseImportTypes = $fileNode->getPendingFunctionImports(); - if ($useImportTypes === [] && $constantUseImportTypes === [] && $functionUseImportTypes === []) { - return $nodes; - } - /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); From 61925ab822928509d5decf43f2bd629d70c25d3c Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 12:24:12 +0200 Subject: [PATCH 6/8] add PendingUsedImports value object, to separate from existing UsedImprts --- .../ValueObject/PendingUsedImports.php | 132 ++++++++++++++++++ rules/CodingStyle/Node/NameImporter.php | 12 +- .../NestedAnnotationToAttributeRector.php | 4 +- .../NameImportingPhpDocNodeVisitor.php | 8 +- src/PhpParser/Node/FileNode.php | 120 +--------------- src/PostRector/Rector/UseAddingPostRector.php | 11 +- src/Rector/AbstractRector.php | 2 +- 7 files changed, 162 insertions(+), 127 deletions(-) create mode 100644 rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php diff --git a/rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php b/rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php new file mode 100644 index 00000000000..4e69bc1a34d --- /dev/null +++ b/rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php @@ -0,0 +1,132 @@ +useImports[] = $fullyQualifiedObjectType; + } + + public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->functionImports[] = $fullyQualifiedObjectType; + } + + public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->constantImports[] = $fullyQualifiedObjectType; + } + + public function hasPendingUseImports(): bool + { + if ($this->useImports !== []) { + return true; + } + + if ($this->functionImports !== []) { + return true; + } + + return $this->constantImports !== []; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getUseImports(): array + { + return $this->useImports; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getFunctionImports(): array + { + return $this->functionImports; + } + + /** + * @return FullyQualifiedObjectType[] + */ + public function getConstantImports(): array + { + return $this->constantImports; + } + + public function isShortImported(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $shortName = $fullyQualifiedObjectType->getShortName(); + + foreach ($this->constantImports as $constantImport) { + // don't compare strtolower for use const as insensitive is allowed, see https://3v4l.org/lteVa + if ($constantImport->getShortName() === $shortName) { + return true; + } + } + + $shortName = strtolower($shortName); + + foreach ($this->useImports as $useImport) { + if (strtolower($useImport->getShortName()) === $shortName) { + return true; + } + } + + foreach ($this->functionImports as $functionImport) { + if (strtolower($functionImport->getShortName()) === $shortName) { + return true; + } + } + + return false; + } + + public function isImportShortable(FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + foreach ($this->useImports as $useImport) { + if ($fullyQualifiedObjectType->equals($useImport)) { + return true; + } + } + + foreach ($this->constantImports as $constantImport) { + if ($fullyQualifiedObjectType->equals($constantImport)) { + return true; + } + } + + foreach ($this->functionImports as $functionImport) { + if ($fullyQualifiedObjectType->equals($functionImport)) { + return true; + } + } + + return false; + } +} diff --git a/rules/CodingStyle/Node/NameImporter.php b/rules/CodingStyle/Node/NameImporter.php index b8eceaef4d4..56f9cfcd2e4 100644 --- a/rules/CodingStyle/Node/NameImporter.php +++ b/rules/CodingStyle/Node/NameImporter.php @@ -104,8 +104,9 @@ private function importNameAndCollectNewUseStatement( return null; } - if ($fileNode->isShortImported($fullyQualifiedObjectType)) { - if ($fileNode->isImportShortable($fullyQualifiedObjectType)) { + $pendingUsedImports = $fileNode->getPendingUsedImports(); + if ($pendingUsedImports->isShortImported($fullyQualifiedObjectType)) { + if ($pendingUsedImports->isImportShortable($fullyQualifiedObjectType)) { return $fullyQualifiedObjectType->getShortNameNode(); } @@ -147,12 +148,13 @@ private function addUseImport( return; } + $pendingUsedImports = $fileNode->getPendingUsedImports(); if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === true) { - $fileNode->addFunctionUseImport($fullyQualifiedObjectType); + $pendingUsedImports->addFunctionUseImport($fullyQualifiedObjectType); } elseif ($fullyQualified->getAttribute(AttributeKey::IS_CONSTFETCH_NAME) === true) { - $fileNode->addConstantUseImport($fullyQualifiedObjectType); + $pendingUsedImports->addConstantUseImport($fullyQualifiedObjectType); } else { - $fileNode->addUseImport($fullyQualifiedObjectType); + $pendingUsedImports->addUseImport($fullyQualifiedObjectType); } } } diff --git a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php index c1e565b09c6..fa604b3cdf2 100644 --- a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php @@ -216,6 +216,8 @@ private function completeExtraUseImports(array $attributeGroups): void return; } + $pendingUsedImports = $fileNode->getPendingUsedImports(); + foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attr) { $namespacedAttrName = $attr->name->getAttribute(AttributeKey::EXTRA_USE_IMPORT); @@ -223,7 +225,7 @@ private function completeExtraUseImports(array $attributeGroups): void continue; } - $fileNode->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); + $pendingUsedImports->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); } } } diff --git a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php index 0e0e50f39e9..b8829365ffc 100644 --- a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php +++ b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php @@ -143,14 +143,16 @@ private function processFqnNameImport( return null; } + $pendingUsedImports = $fileNode->getPendingUsedImports(); + // should skip because its already used - if ($fileNode->isShortImported($fullyQualifiedObjectType) - && ! $fileNode->isImportShortable($fullyQualifiedObjectType)) { + if ($pendingUsedImports->isShortImported($fullyQualifiedObjectType) + && ! $pendingUsedImports->isImportShortable($fullyQualifiedObjectType)) { return null; } if ($this->shouldImport($fileNode, $newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { - $fileNode->addUseImport($fullyQualifiedObjectType); + $pendingUsedImports->addUseImport($fullyQualifiedObjectType); $this->hasChanged = true; return $newNode; diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index 187270376c9..ad6626e5ab0 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -13,6 +13,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Use_; +use Rector\CodingStyle\ClassNameImport\ValueObject\PendingUsedImports; use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; @@ -25,19 +26,8 @@ class FileNode extends Stmt { /** * Imports queued to be added on the next UseAddingPostRector run; scoped to this file - * @var FullyQualifiedObjectType[] */ - private array $pendingUseImports = []; - - /** - * @var FullyQualifiedObjectType[] - */ - private array $pendingFunctionImports = []; - - /** - * @var FullyQualifiedObjectType[] - */ - private array $pendingConstantImports = []; + private PendingUsedImports $pendingUsedImports; /** * @param Stmt[] $stmts @@ -47,6 +37,8 @@ public function __construct( public array $stmts, private UsedImports $usedImports, ) { + $this->pendingUsedImports = new PendingUsedImports(); + $firstStmt = $stmts[0] ?? null; $attributes = $firstStmt instanceof Node ? $firstStmt->getAttributes() : []; @@ -141,56 +133,9 @@ public function addImports( return true; } - public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - $this->pendingUseImports[] = $fullyQualifiedObjectType; - } - - public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - $this->pendingFunctionImports[] = $fullyQualifiedObjectType; - } - - public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void - { - $this->pendingConstantImports[] = $fullyQualifiedObjectType; - } - - public function hasPendingUseImports(): bool - { - if ($this->pendingUseImports !== []) { - return true; - } - - if ($this->pendingFunctionImports !== []) { - return true; - } - - return $this->pendingConstantImports !== []; - } - - /** - * @return FullyQualifiedObjectType[] - */ - public function getPendingUseImports(): array - { - return $this->pendingUseImports; - } - - /** - * @return FullyQualifiedObjectType[] - */ - public function getPendingFunctionImports(): array + public function getPendingUsedImports(): PendingUsedImports { - return $this->pendingFunctionImports; - } - - /** - * @return FullyQualifiedObjectType[] - */ - public function getPendingConstantImports(): array - { - return $this->pendingConstantImports; + return $this->pendingUsedImports; } public function hasImport(FullyQualifiedObjectType $fullyQualifiedObjectType): bool @@ -204,57 +149,6 @@ public function hasImport(FullyQualifiedObjectType $fullyQualifiedObjectType): b return false; } - public function isShortImported(FullyQualifiedObjectType $fullyQualifiedObjectType): bool - { - $shortName = $fullyQualifiedObjectType->getShortName(); - - foreach ($this->pendingConstantImports as $pendingConstantImport) { - // don't compare strtolower for use const as insensitive is allowed, see https://3v4l.org/lteVa - if ($pendingConstantImport->getShortName() === $shortName) { - return true; - } - } - - $shortName = strtolower($shortName); - - foreach ($this->pendingUseImports as $pendingUseImport) { - if (strtolower($pendingUseImport->getShortName()) === $shortName) { - return true; - } - } - - foreach ($this->pendingFunctionImports as $pendingFunctionImport) { - if (strtolower($pendingFunctionImport->getShortName()) === $shortName) { - return true; - } - } - - return false; - } - - public function isImportShortable(FullyQualifiedObjectType $fullyQualifiedObjectType): bool - { - foreach ($this->pendingUseImports as $pendingUseImport) { - if ($fullyQualifiedObjectType->equals($pendingUseImport)) { - return true; - } - } - - foreach ($this->pendingConstantImports as $pendingConstantImport) { - if ($fullyQualifiedObjectType->equals($pendingConstantImport)) { - return true; - } - } - - foreach ($this->pendingFunctionImports as $pendingFunctionImport) { - if ($fullyQualifiedObjectType->equals($pendingFunctionImport)) { - return true; - } - } - - return false; - } - /** * The queued use imports merged with the use imports already present in the file * @@ -262,7 +156,7 @@ public function isImportShortable(FullyQualifiedObjectType $fullyQualifiedObject */ public function resolveUsedImportTypes(): array { - $objectTypes = $this->pendingUseImports; + $objectTypes = $this->pendingUsedImports->getUseImports(); foreach ($this->getUsesAndGroupUses() as $use) { $prefix = $use instanceof GroupUse ? $use->prefix . '\\' : ''; diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index dcedced4622..e114aec7d7e 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -28,7 +28,8 @@ public function shouldTraverse(array $stmts): bool return false; } - return $fileNode->hasPendingUseImports(); + return $fileNode->getPendingUsedImports() + ->hasPendingUseImports(); } /** @@ -40,9 +41,11 @@ public function beforeTraverse(array $nodes): array /** @var FileNode $fileNode */ $fileNode = $nodes[0] ?? null; - $useImportTypes = $fileNode->getPendingUseImports(); - $constantUseImportTypes = $fileNode->getPendingConstantImports(); - $functionUseImportTypes = $fileNode->getPendingFunctionImports(); + $pendingUsedImports = $fileNode->getPendingUsedImports(); + + $useImportTypes = $pendingUsedImports->getUseImports(); + $constantUseImportTypes = $pendingUsedImports->getConstantImports(); + $functionUseImportTypes = $pendingUsedImports->getFunctionImports(); /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); diff --git a/src/Rector/AbstractRector.php b/src/Rector/AbstractRector.php index 36ab2ae666d..f472a380b5c 100644 --- a/src/Rector/AbstractRector.php +++ b/src/Rector/AbstractRector.php @@ -58,7 +58,7 @@ abstract class AbstractRector extends NodeVisitorAbstract implements RectorInter protected NodeComparator $nodeComparator; /** - * @deprecated Use getFile() instead. + * @internal Use getFile() instead. */ protected File $file; From ff8da48ebb8ab029ffe3297e261d32a192b4200b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 12:27:16 +0200 Subject: [PATCH 7/8] renaem PendingUsedIMprots to PendignIMports --- .../UsedImportsResolverTest.php | 13 +++++++------ .../{PendingUsedImports.php => PendingImports.php} | 2 +- rules/CodingStyle/Node/NameImporter.php | 14 +++++++------- .../Property/NestedAnnotationToAttributeRector.php | 4 ++-- src/Application/FileProcessor.php | 4 ++-- .../NameImportingPhpDocNodeVisitor.php | 8 ++++---- src/PhpParser/Node/FileNode.php | 12 ++++++------ src/PostRector/Rector/ClassRenamingPostRector.php | 2 +- src/PostRector/Rector/UseAddingPostRector.php | 12 +++++++----- src/Testing/TestingParser/TestingParser.php | 4 ++-- 10 files changed, 39 insertions(+), 36 deletions(-) rename rules/CodingStyle/ClassNameImport/ValueObject/{PendingUsedImports.php => PendingImports.php} (99%) diff --git a/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/UsedImportsResolverTest.php b/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/UsedImportsResolverTest.php index 7b2292a4dcc..8c5724ab1a9 100644 --- a/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/UsedImportsResolverTest.php +++ b/rules-tests/CodingStyle/ClassNameImport/UsedImportsResolver/UsedImportsResolverTest.php @@ -6,6 +6,7 @@ use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; +use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; use Rector\Testing\PHPUnit\AbstractLazyTestCase; use Rector\Testing\TestingParser\TestingParser; @@ -31,7 +32,7 @@ public function testResolvesUseFunctionAndConstantImports(): void // the class itself, the normal use and the aliased use $useImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (AliasedObjectType|FullyQualifiedObjectType $objectType): string => $objectType->getClassName(), $usedImports->getUseImports() ); @@ -49,7 +50,7 @@ public function testResolvesUseFunctionAndConstantImports(): void ); $functionImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (FullyQualifiedObjectType $fullyQualifiedObjectType): string => $fullyQualifiedObjectType->getClassName(), $usedImports->getFunctionImports() ); $this->assertSame( @@ -58,7 +59,7 @@ public function testResolvesUseFunctionAndConstantImports(): void ); $constantImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (FullyQualifiedObjectType $fullyQualifiedObjectType): string => $fullyQualifiedObjectType->getClassName(), $usedImports->getConstantImports() ); $this->assertSame( @@ -74,7 +75,7 @@ public function testResolvesClassOnlyWhenNoImports(): void $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); $useImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (AliasedObjectType|FullyQualifiedObjectType $objectType): string => $objectType->getClassName(), $usedImports->getUseImports() ); @@ -93,7 +94,7 @@ public function testResolvesImportsInNonNamespacedFile(): void $usedImports = $this->usedImportsResolver->resolveForStmts($stmts); $useImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (AliasedObjectType|FullyQualifiedObjectType $objectType): string => $objectType->getClassName(), $usedImports->getUseImports() ); @@ -103,7 +104,7 @@ public function testResolvesImportsInNonNamespacedFile(): void ], $useImportNames); $functionImportNames = array_map( - static fn ($objectType): string => $objectType->getClassName(), + static fn (FullyQualifiedObjectType $fullyQualifiedObjectType): string => $fullyQualifiedObjectType->getClassName(), $usedImports->getFunctionImports() ); $this->assertSame( diff --git a/rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php b/rules/CodingStyle/ClassNameImport/ValueObject/PendingImports.php similarity index 99% rename from rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php rename to rules/CodingStyle/ClassNameImport/ValueObject/PendingImports.php index 4e69bc1a34d..d76178b5a7a 100644 --- a/rules/CodingStyle/ClassNameImport/ValueObject/PendingUsedImports.php +++ b/rules/CodingStyle/ClassNameImport/ValueObject/PendingImports.php @@ -10,7 +10,7 @@ * Imports queued to be added on the next UseAddingPostRector run; scoped to a single file. * Unlike UsedImports, this is mutable and collected during the post-rector chain. */ -final class PendingUsedImports +final class PendingImports { /** * @var FullyQualifiedObjectType[] diff --git a/rules/CodingStyle/Node/NameImporter.php b/rules/CodingStyle/Node/NameImporter.php index 56f9cfcd2e4..c2e90dd3d19 100644 --- a/rules/CodingStyle/Node/NameImporter.php +++ b/rules/CodingStyle/Node/NameImporter.php @@ -104,9 +104,9 @@ private function importNameAndCollectNewUseStatement( return null; } - $pendingUsedImports = $fileNode->getPendingUsedImports(); - if ($pendingUsedImports->isShortImported($fullyQualifiedObjectType)) { - if ($pendingUsedImports->isImportShortable($fullyQualifiedObjectType)) { + $pendingImports = $fileNode->getPendingImports(); + if ($pendingImports->isShortImported($fullyQualifiedObjectType)) { + if ($pendingImports->isImportShortable($fullyQualifiedObjectType)) { return $fullyQualifiedObjectType->getShortNameNode(); } @@ -148,13 +148,13 @@ private function addUseImport( return; } - $pendingUsedImports = $fileNode->getPendingUsedImports(); + $pendingImports = $fileNode->getPendingImports(); if ($fullyQualified->getAttribute(AttributeKey::IS_FUNCCALL_NAME) === true) { - $pendingUsedImports->addFunctionUseImport($fullyQualifiedObjectType); + $pendingImports->addFunctionUseImport($fullyQualifiedObjectType); } elseif ($fullyQualified->getAttribute(AttributeKey::IS_CONSTFETCH_NAME) === true) { - $pendingUsedImports->addConstantUseImport($fullyQualifiedObjectType); + $pendingImports->addConstantUseImport($fullyQualifiedObjectType); } else { - $pendingUsedImports->addUseImport($fullyQualifiedObjectType); + $pendingImports->addUseImport($fullyQualifiedObjectType); } } } diff --git a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php index fa604b3cdf2..2de4cc0b888 100644 --- a/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php +++ b/rules/Php80/Rector/Property/NestedAnnotationToAttributeRector.php @@ -216,7 +216,7 @@ private function completeExtraUseImports(array $attributeGroups): void return; } - $pendingUsedImports = $fileNode->getPendingUsedImports(); + $pendingImports = $fileNode->getPendingImports(); foreach ($attributeGroups as $attributeGroup) { foreach ($attributeGroup->attrs as $attr) { @@ -225,7 +225,7 @@ private function completeExtraUseImports(array $attributeGroups): void continue; } - $pendingUsedImports->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); + $pendingImports->addUseImport(new FullyQualifiedObjectType($namespacedAttrName)); } } } diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 8eeaf3ec03b..1fc05375392 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -176,11 +176,11 @@ private function parseFileNodes(File $file, bool $forNewestSupportedVersion = tr // resolve names up front, so used imports (incl. the class FQN) are resolvable at construction, // before scope decoration runs; only annotates namespacedName, does not replace name nodes - $nameResolvingTraverser = new NodeTraverser(new NameResolver(null, [ + $nameResolvingNodeTraverser = new NodeTraverser(new NameResolver(null, [ 'preserveOriginalNames' => true, 'replaceNodes' => false, ])); - $nameResolvingTraverser->traverse($oldStmts); + $nameResolvingNodeTraverser->traverse($oldStmts); // wrap in FileNode to allow file-level rules; seed used imports once, kept in sync incrementally $usedImports = $this->usedImportsResolver->resolveForStmts($oldStmts); diff --git a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php index b8829365ffc..6a8be942bc4 100644 --- a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php +++ b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php @@ -143,16 +143,16 @@ private function processFqnNameImport( return null; } - $pendingUsedImports = $fileNode->getPendingUsedImports(); + $pendingImports = $fileNode->getPendingImports(); // should skip because its already used - if ($pendingUsedImports->isShortImported($fullyQualifiedObjectType) - && ! $pendingUsedImports->isImportShortable($fullyQualifiedObjectType)) { + if ($pendingImports->isShortImported($fullyQualifiedObjectType) + && ! $pendingImports->isImportShortable($fullyQualifiedObjectType)) { return null; } if ($this->shouldImport($fileNode, $newNode, $identifierTypeNode, $fullyQualifiedObjectType)) { - $pendingUsedImports->addUseImport($fullyQualifiedObjectType); + $pendingImports->addUseImport($fullyQualifiedObjectType); $this->hasChanged = true; return $newNode; diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php index ad6626e5ab0..8d818e993d9 100644 --- a/src/PhpParser/Node/FileNode.php +++ b/src/PhpParser/Node/FileNode.php @@ -13,7 +13,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Nop; use PhpParser\Node\Stmt\Use_; -use Rector\CodingStyle\ClassNameImport\ValueObject\PendingUsedImports; +use Rector\CodingStyle\ClassNameImport\ValueObject\PendingImports; use Rector\CodingStyle\ClassNameImport\ValueObject\UsedImports; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; @@ -27,7 +27,7 @@ class FileNode extends Stmt /** * Imports queued to be added on the next UseAddingPostRector run; scoped to this file */ - private PendingUsedImports $pendingUsedImports; + private readonly PendingImports $pendingImports; /** * @param Stmt[] $stmts @@ -37,7 +37,7 @@ public function __construct( public array $stmts, private UsedImports $usedImports, ) { - $this->pendingUsedImports = new PendingUsedImports(); + $this->pendingImports = new PendingImports(); $firstStmt = $stmts[0] ?? null; $attributes = $firstStmt instanceof Node ? $firstStmt->getAttributes() : []; @@ -133,9 +133,9 @@ public function addImports( return true; } - public function getPendingUsedImports(): PendingUsedImports + public function getPendingImports(): PendingImports { - return $this->pendingUsedImports; + return $this->pendingImports; } public function hasImport(FullyQualifiedObjectType $fullyQualifiedObjectType): bool @@ -156,7 +156,7 @@ public function hasImport(FullyQualifiedObjectType $fullyQualifiedObjectType): b */ public function resolveUsedImportTypes(): array { - $objectTypes = $this->pendingUsedImports->getUseImports(); + $objectTypes = $this->pendingImports->getUseImports(); foreach ($this->getUsesAndGroupUses() as $use) { $prefix = $use instanceof GroupUse ? $use->prefix . '\\' : ''; diff --git a/src/PostRector/Rector/ClassRenamingPostRector.php b/src/PostRector/Rector/ClassRenamingPostRector.php index ec17e2a8524..0ff5e3ce395 100644 --- a/src/PostRector/Rector/ClassRenamingPostRector.php +++ b/src/PostRector/Rector/ClassRenamingPostRector.php @@ -33,7 +33,7 @@ public function enterNode(Node $node): FileNode|int // keep only the uses that were actually renamed $removedUses = array_values(array_filter( $this->renamedClassesDataCollector->getOldClasses(), - fn (string $removedUse): bool => $this->renamedNameCollector->has($removedUse) + $this->renamedNameCollector->has(...) )); if ($node->removeImports($removedUses)) { diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index e114aec7d7e..ae9821fe1df 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -4,6 +4,7 @@ namespace Rector\PostRector\Rector; +use Override; use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\NodeVisitor; @@ -21,6 +22,7 @@ public function __construct( /** * @param Stmt[] $stmts */ + #[Override] public function shouldTraverse(array $stmts): bool { $fileNode = $stmts[0] ?? null; @@ -28,7 +30,7 @@ public function shouldTraverse(array $stmts): bool return false; } - return $fileNode->getPendingUsedImports() + return $fileNode->getPendingImports() ->hasPendingUseImports(); } @@ -41,11 +43,11 @@ public function beforeTraverse(array $nodes): array /** @var FileNode $fileNode */ $fileNode = $nodes[0] ?? null; - $pendingUsedImports = $fileNode->getPendingUsedImports(); + $pendingImports = $fileNode->getPendingImports(); - $useImportTypes = $pendingUsedImports->getUseImports(); - $constantUseImportTypes = $pendingUsedImports->getConstantImports(); - $functionUseImportTypes = $pendingUsedImports->getFunctionImports(); + $useImportTypes = $pendingImports->getUseImports(); + $constantUseImportTypes = $pendingImports->getConstantImports(); + $functionUseImportTypes = $pendingImports->getFunctionImports(); /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); diff --git a/src/Testing/TestingParser/TestingParser.php b/src/Testing/TestingParser/TestingParser.php index d85091217d4..8ea448a32ba 100644 --- a/src/Testing/TestingParser/TestingParser.php +++ b/src/Testing/TestingParser/TestingParser.php @@ -59,11 +59,11 @@ private function parseToFileAndStmts(string $filePath): array // resolve names up front, so used imports are resolvable at construction, before decoration; // only annotates namespacedName, does not replace name nodes - $nameResolvingTraverser = new NodeTraverser(new NameResolver(null, [ + $nameResolvingNodeTraverser = new NodeTraverser(new NameResolver(null, [ 'preserveOriginalNames' => true, 'replaceNodes' => false, ])); - $stmts = $nameResolvingTraverser->traverse($stmts); + $stmts = $nameResolvingNodeTraverser->traverse($stmts); // wrap in FileNode to enable file-level rules; seed used imports once, kept in sync incrementally $stmts = [new FileNode($stmts, $this->usedImportsResolver->resolveForStmts($stmts))]; From 66961c8402ffbe793755021c54a94ae499304f1d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 17 Jun 2026 12:45:34 +0200 Subject: [PATCH 8/8] add BC layer for removed UseNodesToAddCollector, delegating to FileNode with deprecation warnings --- rector.php | 6 + .../Collector/UseNodesToAddCollector.php | 286 ++++++++++++++++++ 2 files changed, 292 insertions(+) create mode 100644 src/PostRector/Collector/UseNodesToAddCollector.php diff --git a/rector.php b/rector.php index 20e9a368c50..d61e3724f72 100644 --- a/rector.php +++ b/rector.php @@ -4,6 +4,7 @@ use Rector\CodingStyle\Rector\String_\UseClassKeywordForClassNameResolutionRector; use Rector\Config\RectorConfig; +use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPublicMethodParameterRector; use Rector\DeadCode\Rector\ConstFetch\RemovePhpVersionIdCheckRector; use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector; use Rector\PHPUnit\CodeQuality\Rector\Class_\AddSeeTestAnnotationRector; @@ -50,6 +51,11 @@ // keep configs untouched, as the classes are just strings UseClassKeywordForClassNameResolutionRector::class => [__DIR__ . '/config', '*/config/*'], + // BC layer + RemoveUnusedPublicMethodParameterRector::class => [ + __DIR__ . '/src/PostRector/Collector/UseNodesToAddCollector.php', + ], + RemovePhpVersionIdCheckRector::class => [ __DIR__ . '/src/Util/FileHasher.php', __DIR__ . '/src/Configuration/RectorConfigBuilder.php', diff --git a/src/PostRector/Collector/UseNodesToAddCollector.php b/src/PostRector/Collector/UseNodesToAddCollector.php new file mode 100644 index 00000000000..c845f1334a2 --- /dev/null +++ b/src/PostRector/Collector/UseNodesToAddCollector.php @@ -0,0 +1,286 @@ +useNodesToAddCollector->hasImport($file, $objectType)) { + * return; + * } + * + * $this->useNodesToAddCollector->addUseImport($objectType); + * } + * + * After: + * + * public function refactor(File $file, FullyQualifiedObjectType $objectType): void + * { + * $fileNode = $file->getFileNode(); + * if (! $fileNode instanceof FileNode) { + * return; + * } + * + * if ($fileNode->hasImport($objectType)) { + * return; + * } + * + * $fileNode->getPendingImports() + * ->addUseImport($objectType); + * } + * + * Method mapping: + * addUseImport($t) -> $file->getFileNode()->getPendingImports()->addUseImport($t) + * addConstantUseImport($t) -> $file->getFileNode()->getPendingImports()->addConstantUseImport($t) + * addFunctionUseImport($t) -> $file->getFileNode()->getPendingImports()->addFunctionUseImport($t) + * getUseImportTypesByNode($file) -> $file->getFileNode()->resolveUsedImportTypes() + * hasImport($file, $t) -> $file->getFileNode()->hasImport($t) + * isShortImported($file, $t) -> $file->getFileNode()->getPendingImports()->isShortImported($t) + * isImportShortable($file, $t) -> $file->getFileNode()->getPendingImports()->isImportShortable($t) + * getObjectImportsByFilePath() -> $file->getFileNode()->getPendingImports()->getUseImports() + * getConstantImportsByFilePath() -> $file->getFileNode()->getPendingImports()->getConstantImports() + * getFunctionImportsByFilePath() -> $file->getFileNode()->getPendingImports()->getFunctionImports() + */ +final readonly class UseNodesToAddCollector +{ + public function __construct( + private CurrentFileProvider $currentFileProvider, + ) { + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->addUseImport($type) instead. + */ + public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->warn('addUseImport()', '$file->getFileNode()->getPendingImports()->addUseImport($type)'); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return; + } + + $fileNode->getPendingImports() + ->addUseImport($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->addConstantUseImport($type) instead. + */ + public function addConstantUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->warn('addConstantUseImport()', '$file->getFileNode()->getPendingImports()->addConstantUseImport($type)'); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return; + } + + $fileNode->getPendingImports() + ->addConstantUseImport($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->addFunctionUseImport($type) instead. + */ + public function addFunctionUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void + { + $this->warn('addFunctionUseImport()', '$file->getFileNode()->getPendingImports()->addFunctionUseImport($type)'); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return; + } + + $fileNode->getPendingImports() + ->addFunctionUseImport($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->resolveUsedImportTypes() instead. + * + * @return array + */ + public function getUseImportTypesByNode(File $file): array + { + $this->warn('getUseImportTypesByNode()', '$file->getFileNode()->resolveUsedImportTypes()'); + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return []; + } + + return $fileNode->resolveUsedImportTypes(); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->hasImport($type) instead. + */ + public function hasImport(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $this->warn('hasImport()', '$file->getFileNode()->hasImport($type)'); + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return false; + } + + return $fileNode->hasImport($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->isShortImported($type) instead. + */ + public function isShortImported(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $this->warn('isShortImported()', '$file->getFileNode()->getPendingImports()->isShortImported($type)'); + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return false; + } + + return $fileNode->getPendingImports() + ->isShortImported($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->isImportShortable($type) instead. + */ + public function isImportShortable(File $file, FullyQualifiedObjectType $fullyQualifiedObjectType): bool + { + $this->warn('isImportShortable()', '$file->getFileNode()->getPendingImports()->isImportShortable($type)'); + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { + return false; + } + + return $fileNode->getPendingImports() + ->isImportShortable($fullyQualifiedObjectType); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->getUseImports() instead. + * + * @return FullyQualifiedObjectType[] + */ + public function getObjectImportsByFilePath(string $filePath): array + { + $this->warn('getObjectImportsByFilePath()', '$file->getFileNode()->getPendingImports()->getUseImports()'); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return []; + } + + return $fileNode->getPendingImports() + ->getUseImports(); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->getConstantImports() instead. + * + * @return FullyQualifiedObjectType[] + */ + public function getConstantImportsByFilePath(string $filePath): array + { + $this->warn( + 'getConstantImportsByFilePath()', + '$file->getFileNode()->getPendingImports()->getConstantImports()' + ); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return []; + } + + return $fileNode->getPendingImports() + ->getConstantImports(); + } + + /** + * @api + * + * @deprecated Use $file->getFileNode()->getPendingImports()->getFunctionImports() instead. + * + * @return FullyQualifiedObjectType[] + */ + public function getFunctionImportsByFilePath(string $filePath): array + { + $this->warn( + 'getFunctionImportsByFilePath()', + '$file->getFileNode()->getPendingImports()->getFunctionImports()' + ); + + $fileNode = $this->resolveCurrentFileNode(); + if (! $fileNode instanceof FileNode) { + return []; + } + + return $fileNode->getPendingImports() + ->getFunctionImports(); + } + + private function resolveCurrentFileNode(): ?FileNode + { + $file = $this->currentFileProvider->getFile(); + if (! $file instanceof File) { + return null; + } + + return $file->getFileNode(); + } + + private function warn(string $method, string $replacement): void + { + trigger_error( + sprintf( + 'UseNodesToAddCollector::%s is deprecated and will be removed. Use "%s" instead, via $file->getFileNode().', + $method, + $replacement + ), + E_USER_DEPRECATED + ); + } +}