From e50cb9403a7e580f85b4b26417314cd27e697a88 Mon Sep 17 00:00:00 2001 From: Guillaume Sainthillier Date: Mon, 12 Oct 2026 13:09:16 +0200 Subject: [PATCH 1/3] [perf] Avoid re-parsing current file in RemoveUnusedPrivateClassConstantRector ClassConstManipulator::hasClassConstFetch() looks for constant fetches in every class ancestor resolved through AstResolver. ClassReflection::getAncestors() includes the class itself, so for each file with a private constant, the file being processed was parsed again and its PHPStan scope fully resolved again. The rule now subscribes to Class_/Enum_ and passes the in-memory class node, which is searched instead of the re-parsed one. Unused constants are removed only after all constants are checked, so the result does not depend on their order. --- ...RemoveUnusedPrivateClassConstantRector.php | 41 ++++++++++++++----- src/NodeManipulator/ClassConstManipulator.php | 15 +++++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/rules/DeadCode/Rector/ClassConst/RemoveUnusedPrivateClassConstantRector.php b/rules/DeadCode/Rector/ClassConst/RemoveUnusedPrivateClassConstantRector.php index dbcd0ff4f8d..07ad4e1a5e8 100644 --- a/rules/DeadCode/Rector/ClassConst/RemoveUnusedPrivateClassConstantRector.php +++ b/rules/DeadCode/Rector/ClassConst/RemoveUnusedPrivateClassConstantRector.php @@ -5,8 +5,9 @@ namespace Rector\DeadCode\Rector\ClassConst; use PhpParser\Node; +use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassConst; -use PhpParser\NodeVisitor; +use PhpParser\Node\Stmt\Enum_; use PHPStan\Reflection\ClassReflection; use Rector\NodeManipulator\ClassConstManipulator; use Rector\PHPStan\ScopeFetcher; @@ -58,28 +59,46 @@ public function run() */ public function getNodeTypes(): array { - return [ClassConst::class]; + return [Class_::class, Enum_::class]; } /** - * @param ClassConst $node + * @param Class_|Enum_ $node */ - public function refactor(Node $node): ?int + public function refactor(Node $node): ?Node { - if ($this->shouldSkipClassConst($node)) { - return null; + $unusedClassConstKeys = []; + foreach ($node->stmts as $key => $stmt) { + if (! $stmt instanceof ClassConst) { + continue; + } + + if ($this->shouldSkipClassConst($stmt)) { + continue; + } + + $classReflection = $this->reflectionResolver->resolveClassReflection($stmt); + if (! $classReflection instanceof ClassReflection) { + continue; + } + + if ($this->classConstManipulator->hasClassConstFetch($stmt, $classReflection, $node)) { + continue; + } + + $unusedClassConstKeys[] = $key; } - $classReflection = $this->reflectionResolver->resolveClassReflection($node); - if (! $classReflection instanceof ClassReflection) { + if ($unusedClassConstKeys === []) { return null; } - if ($this->classConstManipulator->hasClassConstFetch($node, $classReflection)) { - return null; + // remove only after all constants are checked, so a constant used by another removed one is kept + foreach ($unusedClassConstKeys as $unusedClassConstKey) { + unset($node->stmts[$unusedClassConstKey]); } - return NodeVisitor::REMOVE_NODE; + return $node; } private function shouldSkipClassConst(ClassConst $classConst): bool diff --git a/src/NodeManipulator/ClassConstManipulator.php b/src/NodeManipulator/ClassConstManipulator.php index 5a88103c032..35f51a9124a 100644 --- a/src/NodeManipulator/ClassConstManipulator.php +++ b/src/NodeManipulator/ClassConstManipulator.php @@ -26,8 +26,15 @@ public function __construct( ) { } - public function hasClassConstFetch(ClassConst $classConst, ClassReflection $classReflection): bool - { + /** + * @param ClassLike|null $classLike the already parsed class owning the constant; passing it avoids + * re-parsing and re-resolving scope of the current file, which is costly + */ + public function hasClassConstFetch( + ClassConst $classConst, + ClassReflection $classReflection, + ?ClassLike $classLike = null + ): bool { if (! $classReflection->isClass() && ! $classReflection->isEnum()) { return true; } @@ -35,7 +42,9 @@ public function hasClassConstFetch(ClassConst $classConst, ClassReflection $clas $className = $classReflection->getName(); $objectType = new ObjectType($className); foreach ($classReflection->getAncestors() as $ancestorClassReflection) { - $ancestorClass = $this->astResolver->resolveClassFromClassReflection($ancestorClassReflection); + $ancestorClass = $classLike instanceof ClassLike && $ancestorClassReflection === $classReflection + ? $classLike + : $this->astResolver->resolveClassFromClassReflection($ancestorClassReflection); if (! $ancestorClass instanceof ClassLike) { continue; From e714542c550c0789d2fc3b41f9f76cd2db069938 Mon Sep 17 00:00:00 2001 From: Guillaume Sainthillier Date: Mon, 12 Oct 2026 13:29:41 +0200 Subject: [PATCH 2/3] Search private constant fetches only in the class and its traits A private constant cannot be fetched from a parent class or an interface, so only the traits still need to be resolved through AstResolver. --- src/NodeManipulator/ClassConstManipulator.php | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/NodeManipulator/ClassConstManipulator.php b/src/NodeManipulator/ClassConstManipulator.php index 35f51a9124a..faea1f77401 100644 --- a/src/NodeManipulator/ClassConstManipulator.php +++ b/src/NodeManipulator/ClassConstManipulator.php @@ -26,14 +26,10 @@ public function __construct( ) { } - /** - * @param ClassLike|null $classLike the already parsed class owning the constant; passing it avoids - * re-parsing and re-resolving scope of the current file, which is costly - */ public function hasClassConstFetch( ClassConst $classConst, ClassReflection $classReflection, - ?ClassLike $classLike = null + ClassLike $classLike ): bool { if (! $classReflection->isClass() && ! $classReflection->isEnum()) { return true; @@ -41,17 +37,11 @@ public function hasClassConstFetch( $className = $classReflection->getName(); $objectType = new ObjectType($className); - foreach ($classReflection->getAncestors() as $ancestorClassReflection) { - $ancestorClass = $classLike instanceof ClassLike && $ancestorClassReflection === $classReflection - ? $classLike - : $this->astResolver->resolveClassFromClassReflection($ancestorClassReflection); - if (! $ancestorClass instanceof ClassLike) { - continue; - } - - // has in class? - $isClassConstFetchFound = (bool) $this->betterNodeFinder->findFirst($ancestorClass, function (Node $node) use ( + // private constant can be fetched only in the class itself, or in a trait it uses + $classLikes = [$classLike, ...$this->astResolver->parseClassReflectionTraits($classReflection)]; + foreach ($classLikes as $currentClassLike) { + $isClassConstFetchFound = (bool) $this->betterNodeFinder->findFirst($currentClassLike, function (Node $node) use ( $classConst, $className, $objectType From ecebdd16e87c7f86891b3bd51c4df5d9195a507a Mon Sep 17 00:00:00 2001 From: Guillaume Sainthillier Date: Mon, 12 Oct 2026 13:46:48 +0200 Subject: [PATCH 3/3] Resolve traits only when the class uses any --- src/NodeManipulator/ClassConstManipulator.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/NodeManipulator/ClassConstManipulator.php b/src/NodeManipulator/ClassConstManipulator.php index faea1f77401..bed99fd8a6b 100644 --- a/src/NodeManipulator/ClassConstManipulator.php +++ b/src/NodeManipulator/ClassConstManipulator.php @@ -39,7 +39,11 @@ public function hasClassConstFetch( $objectType = new ObjectType($className); // private constant can be fetched only in the class itself, or in a trait it uses - $classLikes = [$classLike, ...$this->astResolver->parseClassReflectionTraits($classReflection)]; + $classLikes = [$classLike]; + if ($classLike->getTraitUses() !== []) { + $classLikes = [...$classLikes, ...$this->astResolver->parseClassReflectionTraits($classReflection)]; + } + foreach ($classLikes as $currentClassLike) { $isClassConstFetchFound = (bool) $this->betterNodeFinder->findFirst($currentClassLike, function (Node $node) use ( $classConst,