From ed47d6ba00518337ff0cc7ae653caf193f04a1d9 Mon Sep 17 00:00:00 2001 From: acoulton Date: Thu, 18 Dec 2025 01:03:07 +0000 Subject: [PATCH 1/3] test: Prove ArrayToFirstClassCallableRector skips internal callbacks If a class contains array callbacks with a non-public method, they are not converted into first-class callables. This is due to a fix for a bug that was specific to skipping non-public methods referenced from *outside* the owning class - there was no previous coverage for what should happen when a callable is referenced within a valid scope. --- .../some_class_with_own_private.php.inc | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc new file mode 100644 index 00000000000..a4f4f7bf6c2 --- /dev/null +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc @@ -0,0 +1,35 @@ + +----- +name(...); + } + + private function name() + { + } +} + +?> From 339ec9cd54d15c18e675e662eadc1123d0075375 Mon Sep 17 00:00:00 2001 From: acoulton Date: Thu, 18 Dec 2025 01:14:11 +0000 Subject: [PATCH 2/3] fix: ArrayToFirstClassCallableRector should convert private in own scope If an array callable is explicitly referencing `$this`, then it should always be safe to convert it to a first-class callable even if the method is private / protected. This was the original behaviour of the 8.1 fixers, but it changed in v1.1.1 following a fix for cases where a private / protected method was being referenced from outside the owning scope. --- .../ArrayToFirstClassCallableRector.php | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php index 390cd36d440..7112ac968d8 100644 --- a/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php +++ b/rules/Php81/Rector/Array_/ArrayToFirstClassCallableRector.php @@ -72,6 +72,7 @@ public function name() } } CODE_SAMPLE + , ), ]); } @@ -123,7 +124,7 @@ public function refactor(Node $node): StaticCall|MethodCall|null if ($type instanceof FullyQualifiedObjectType && $this->isNonStaticOtherObject( $type, $arrayCallable, - $scope + $scope, )) { return null; } @@ -133,13 +134,9 @@ public function refactor(Node $node): StaticCall|MethodCall|null $methodName = $arrayCallable->getMethod(); $methodCall = new MethodCall($callerExpr, $methodName, $args); - $classReflection = $this->reflectionResolver->resolveClassReflectionSourceObject($methodCall); - if ($classReflection instanceof ClassReflection && $classReflection->hasNativeMethod($methodName)) { - $method = $classReflection->getNativeMethod($methodName); - if (! $method->isPublic()) { - return null; - } + if ($this->isReferenceToNonPublicMethodOutsideOwningScope($methodCall, $methodName)) { + return null; } return $methodCall; @@ -174,4 +171,25 @@ private function isNonStaticOtherObject( return ! $extendedMethodReflection->isPublic(); } + + private function isReferenceToNonPublicMethodOutsideOwningScope(MethodCall $methodCall, string $methodName): bool + { + if ($methodCall->var instanceof Variable && $methodCall->var->name === 'this') { + // If the callable is scoped to `$this` then it can be converted even if it is protected / private + return false; + } + + // If the callable is scoped to another object / variable then it should only be converted if it is public + // https://github.com/rectorphp/rector/issues/8659 + $classReflection = $this->reflectionResolver->resolveClassReflectionSourceObject($methodCall); + + if ($classReflection instanceof ClassReflection && $classReflection->hasNativeMethod($methodName)) { + $method = $classReflection->getNativeMethod($methodName); + if (! $method->isPublic()) { + return true; + } + } + + return false; + } } From 2da345dec37f2f5bc9c1637992a714e2b95de5d2 Mon Sep 17 00:00:00 2001 From: Andrew Coulton Date: Thu, 18 Dec 2025 08:18:21 +0000 Subject: [PATCH 3/3] Apply fixture class naming changes from code review Co-authored-by: Abdul Malik Ikhsan --- .../Fixture/some_class_with_own_private.php.inc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc index a4f4f7bf6c2..f7563cda6f1 100644 --- a/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc +++ b/rules-tests/Php81/Rector/Array_/ArrayToFirstClassCallableRector/Fixture/some_class_with_own_private.php.inc @@ -2,7 +2,7 @@ namespace Rector\Tests\Php81\Rector\Array_\FirstClassCallableRector\Fixture; -final class SomeClass +final class SomeClassWithOwnPrivate { public function run() { @@ -20,7 +20,7 @@ final class SomeClass namespace Rector\Tests\Php81\Rector\Array_\FirstClassCallableRector\Fixture; -final class SomeClass +final class SomeClassWithOwnPrivate { public function run() {