From bbe683f7db25d9ce539e89e5fca26a4dc6f33173 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 13 Nov 2026 00:37:44 +0700 Subject: [PATCH 1/2] Use ReflectionProvider instead of AstResolver on DowngradeArraySpreadRector to fix class constant spread from other class --- .../on_class_constant_not_movable.php.inc | 41 +++++ .../on_class_constant_other_namespace.php.inc | 29 ++++ ...constant_same_file_other_namespace.php.inc | 49 ++++++ .../Source/ClassWithArrayConstant.php | 10 ++ .../Source/ParentWithRelativeConstant.php | 22 +++ .../Source/SomeTokens.php | 10 ++ .../Array_/DowngradeArraySpreadRector.php | 154 ++++++++++++++---- 7 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_not_movable.php.inc create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_other_namespace.php.inc create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_same_file_other_namespace.php.inc create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithArrayConstant.php create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ParentWithRelativeConstant.php create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/SomeTokens.php diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_not_movable.php.inc b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_not_movable.php.inc new file mode 100644 index 00000000..d575b6cc --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_not_movable.php.inc @@ -0,0 +1,41 @@ + +----- + diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_other_namespace.php.inc b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_other_namespace.php.inc new file mode 100644 index 00000000..0d9ea494 --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_other_namespace.php.inc @@ -0,0 +1,29 @@ + +----- + diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_same_file_other_namespace.php.inc b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_same_file_other_namespace.php.inc new file mode 100644 index 00000000..d94b1f37 --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_same_file_other_namespace.php.inc @@ -0,0 +1,49 @@ + +----- + diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithArrayConstant.php b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithArrayConstant.php new file mode 100644 index 00000000..7d41f433 --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithArrayConstant.php @@ -0,0 +1,10 @@ +refactorUnderClassConst($node); + if ($node instanceof ClassLike) { + return $this->refactorUnderClassLike($node); } if (! $this->arraySpreadAnalyzer->isArrayWithUnpack($node)) { @@ -114,21 +118,41 @@ public function refactor(Node $node): ?Node return $this->arrayMergeFromArraySpreadFactory->createFromArray($node, $scope); } - private function refactorUnderClassConst(ClassConst $classConst): ?ClassConst + private function refactorUnderClassLike(ClassLike $classLike): ?ClassLike + { + $hasChanged = false; + + foreach ($classLike->getConstants() as $classConst) { + if ($this->refactorUnderClassConst($classConst, $classLike)) { + $hasChanged = true; + } + } + + if ($hasChanged) { + return $classLike; + } + + return null; + } + + private function refactorUnderClassConst(ClassConst $classConst, ClassLike $classLike): bool { $arrays = $this->betterNodeFinder->findInstanceOf($classConst->consts, Array_::class); if ($arrays === []) { - return null; + return false; } $hasChanged = false; - $this->traverseNodesWithCallable($classConst->consts, function (Node $subNode) use (&$hasChanged): ?Node { + $this->traverseNodesWithCallable($classConst->consts, function (Node $subNode) use ( + &$hasChanged, + $classLike + ): ?Node { if (! $subNode instanceof Array_) { return null; } - $refactorArrayConstValue = $this->refactorArrayConstValue($subNode); + $refactorArrayConstValue = $this->refactorArrayConstValue($subNode, $classLike); if ($refactorArrayConstValue instanceof Array_) { $hasChanged = true; return $refactorArrayConstValue; @@ -137,11 +161,7 @@ private function refactorUnderClassConst(ClassConst $classConst): ?ClassConst return null; }); - if ($hasChanged) { - return $classConst; - } - - return null; + return $hasChanged; } private function resolveItemType(?ArrayItem $arrayItem): ?Type @@ -169,7 +189,7 @@ private function resolveItemType(?ArrayItem $arrayItem): ?Type return $this->nodeTypeResolver->getType($arrayItem->value->class); } - private function refactorArrayConstValue(Array_ $array): ?Array_ + private function refactorArrayConstValue(Array_ $array, ClassLike $classLike): ?Array_ { $hasChanged = false; @@ -188,29 +208,105 @@ private function refactorArrayConstValue(Array_ $array): ?Array_ $name = $value->name; /** @var Identifier $name */ - $classLike = $this->astResolver->resolveClassFromName($type->getClassName()); - if (! $classLike instanceof ClassLike) { + $constantValueExpr = $this->resolveConstantValueExpr($classLike, $type->getClassName(), $name->toString()); + if (! $constantValueExpr instanceof Array_) { continue; } - $constants = $classLike->getConstants(); + array_pop($newArray->items); + $newArray->items = array_merge($newArray->items, $constantValueExpr->items); - foreach ($constants as $constant) { - $const = $constant->consts[0]; + $hasChanged = true; + } - if ($const->name->toString() === $name->toString() && $const->value instanceof Array_) { - array_pop($newArray->items); - $newArray->items = array_merge($newArray->items, $const->value->items); + if ($hasChanged) { + return $newArray; + } - $hasChanged = true; + return null; + } + + private function resolveConstantValueExpr(ClassLike $classLike, string $className, string $constantName): ?Expr + { + // own class constant is read from current AST, to keep the names as written + if ($this->isName($classLike, $className)) { + foreach ($classLike->getConstants() as $classConst) { + foreach ($classConst->consts as $const) { + if ($this->isName($const, $constantName)) { + return $const->value; + } } } } - if ($hasChanged) { - return $newArray; + if (! $this->reflectionProvider->hasClass($className)) { + return null; } - return null; + $classReflection = $this->reflectionProvider->getClass($className); + if (! $classReflection->hasConstant($constantName)) { + return null; + } + + $valueExpr = $classReflection->getConstant($constantName) + ->getValueExpr(); + + $contextDependentNode = $this->betterNodeFinder->findFirst( + $valueExpr, + fn (Node $node): bool => $this->isContextDependentNode($node) + ); + + return $contextDependentNode instanceof Node ? null : $valueExpr; + } + + /** + * These change meaning, or are no longer accessible, once moved to another class + */ + private function isContextDependentNode(Node $node): bool + { + if ($node instanceof MagicConst) { + return true; + } + + if ($node instanceof Name && $node->isSpecialClassName()) { + return true; + } + + if ($node instanceof ConstFetch) { + return $this->isNamespacedConstFetch($node); + } + + return $node instanceof ClassConstFetch && $this->isNonPublicClassConstFetch($node); + } + + private function isNamespacedConstFetch(ConstFetch $constFetch): bool + { + $namespacedName = $constFetch->name->getAttribute('namespacedName'); + if (! $namespacedName instanceof FullyQualified) { + return false; + } + + return $this->reflectionProvider->hasConstant($namespacedName, null); + } + + private function isNonPublicClassConstFetch(ClassConstFetch $classConstFetch): bool + { + if (! $classConstFetch->class instanceof Name || ! $classConstFetch->name instanceof Identifier) { + return false; + } + + $className = $classConstFetch->class->toString(); + if (! $this->reflectionProvider->hasClass($className)) { + return false; + } + + $classReflection = $this->reflectionProvider->getClass($className); + $constantName = $classConstFetch->name->toString(); + if (! $classReflection->hasConstant($constantName)) { + return false; + } + + return ! $classReflection->getConstant($constantName) + ->isPublic(); } } From bb6e8394b7ac6805a7fa9259ed7d4534331c0b51 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Fri, 13 Nov 2026 00:37:59 +0700 Subject: [PATCH 2/2] add more test --- ...on_class_constant_global_constants.php.inc | 25 +++++++++++++++++++ .../Source/ClassWithGlobalConstants.php | 10 ++++++++ 2 files changed, 35 insertions(+) create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_global_constants.php.inc create mode 100644 rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithGlobalConstants.php diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_global_constants.php.inc b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_global_constants.php.inc new file mode 100644 index 00000000..1a87b126 --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Fixture/on_class_constant_global_constants.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithGlobalConstants.php b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithGlobalConstants.php new file mode 100644 index 00000000..f77538a2 --- /dev/null +++ b/rules-tests/DowngradePhp74/Rector/Array_/DowngradeArraySpreadRector/Source/ClassWithGlobalConstants.php @@ -0,0 +1,10 @@ +