From 18e4e6c2f1bfbfad08ac891a8698d9e319f32669 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 21 Jul 2026 13:11:08 +0700 Subject: [PATCH 1/2] [DeadCode] Skip used by ReflectionMethod as array callable on RemoveUnusedPrivateMethodParameterRector --- bin/rector.php | 2 - ...ed_by_reflection_as_array_callable.php.inc | 17 +++++++ .../SourcePhp81/NotNumber.php | 8 ++- ...moveUnusedPrivateMethodParameterRector.php | 51 +++++++++++++++++++ src/Autoloading/BootstrapFilesIncluder.php | 2 +- 5 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_used_by_reflection_as_array_callable.php.inc diff --git a/bin/rector.php b/bin/rector.php index d42541061cd..07eea9a09cc 100755 --- a/bin/rector.php +++ b/bin/rector.php @@ -6,12 +6,10 @@ use Rector\Bootstrap\AutoloadFileParameterResolver; use Rector\Bootstrap\RectorConfigsResolver; use Rector\ChangesReporting\Output\JsonOutputFormatter; -use Rector\Config\RectorConfig; use Rector\Configuration\Option; use Rector\Console\Style\SymfonyStyleFactory; use Rector\DependencyInjection\LazyContainerFactory; use Rector\DependencyInjection\RectorContainerFactory; -use Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory; use Rector\Util\Reflection\PrivatesAccessor; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_used_by_reflection_as_array_callable.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_used_by_reflection_as_array_callable.php.inc new file mode 100644 index 00000000000..9d45cbea08a --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_used_by_reflection_as_array_callable.php.inc @@ -0,0 +1,17 @@ +getNumberOfParameters(); + } + + private function methodInjection(\stdClass $contextArgument): void + { + } +} diff --git a/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php index 3a004302a46..0f3aea38921 100644 --- a/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php +++ b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php @@ -7,7 +7,11 @@ #[\Attribute] final class NotNumber { - public function __construct($firstValue = null, $secondValue = null, $hey = null, $hi = null) - { + public function __construct( + $firstValue = null, + $secondValue = null, + $hey = null, + $hi = null + ) { } } diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector.php index 8b3d2677ab5..15590b633d2 100644 --- a/rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector.php +++ b/rules/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector.php @@ -5,9 +5,12 @@ namespace Rector\DeadCode\Rector\ClassMethod; use PhpParser\Node; +use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; +use PhpParser\Node\Expr\New_; use PhpParser\Node\Expr\StaticCall; use PhpParser\Node\Identifier; +use PhpParser\Node\Name; use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; @@ -17,7 +20,10 @@ use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; use Rector\Comments\NodeDocBlock\DocBlockUpdater; use Rector\DeadCode\NodeCollector\UnusedParameterResolver; +use Rector\NodeCollector\NodeAnalyzer\ArrayCallableMethodMatcher; +use Rector\NodeCollector\ValueObject\ArrayCallable; use Rector\PhpParser\Node\BetterNodeFinder; +use Rector\PHPStan\ScopeFetcher; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -33,6 +39,7 @@ public function __construct( private readonly DocBlockUpdater $docBlockUpdater, private readonly PhpDocInfoFactory $phpDocInfoFactory, private readonly BetterNodeFinder $betterNodeFinder, + private readonly ArrayCallableMethodMatcher $arrayCallableMethodMatcher, ) { } @@ -91,6 +98,11 @@ public function refactor(Node $node): ?Node continue; } + // method used via reflection, eg: new ReflectionMethod(...[$this, 'method']) + if ($this->isUsedByReflectionMethod($node, (string) $this->getName($classMethod))) { + continue; + } + // early remove callers if (! $this->removeCallerArgs($node, $classMethod, $unusedParameters)) { continue; @@ -120,6 +132,45 @@ public function refactor(Node $node): ?Node return null; } + private function isUsedByReflectionMethod(Class_ $class, string $methodName): bool + { + $scope = ScopeFetcher::fetch($class); + + $reflectionMethodUsage = $this->betterNodeFinder->findFirst( + $class, + function (Node $subNode) use ($class, $scope, $methodName): bool { + if (! $subNode instanceof New_ || ! $subNode->class instanceof Name) { + return false; + } + + if (! $this->isName($subNode->class, 'ReflectionMethod')) { + return false; + } + + if ($subNode->isFirstClassCallable()) { + return false; + } + + foreach ($subNode->getArgs() as $arg) { + if (! $arg->value instanceof Array_) { + continue; + } + + $arrayCallable = $this->arrayCallableMethodMatcher->match($arg->value, $scope, $methodName); + if ($arrayCallable instanceof ArrayCallable + && $this->isName($class, $arrayCallable->getClass()) + && strcasecmp($arrayCallable->getMethod(), $methodName) === 0) { + return true; + } + } + + return false; + } + ); + + return $reflectionMethodUsage instanceof Node; + } + /** * @param Param[] $unusedParameters */ diff --git a/src/Autoloading/BootstrapFilesIncluder.php b/src/Autoloading/BootstrapFilesIncluder.php index 549eda3617b..9a3b81de87b 100644 --- a/src/Autoloading/BootstrapFilesIncluder.php +++ b/src/Autoloading/BootstrapFilesIncluder.php @@ -4,6 +4,7 @@ namespace Rector\Autoloading; +use PHPStan\DependencyInjection\Container; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Exception\ShouldNotHappenException; @@ -11,7 +12,6 @@ use RecursiveIteratorIterator; use SplFileInfo; use Webmozart\Assert\Assert; -use PHPStan\DependencyInjection\Container; /** * @see \Rector\Tests\Autoloading\BootstrapFilesIncluderTest From fcc0d4ceef2c9ae68282bd35d2d6ec30651cfdb4 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Tue, 21 Jul 2026 13:15:32 +0700 Subject: [PATCH 2/2] fix run latest ecs with skip --- ecs.php | 1 + .../AnnotationToAttributeRector/SourcePhp81/NotNumber.php | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/ecs.php b/ecs.php index 3ebd31e9a1b..45bd97c9c41 100644 --- a/ecs.php +++ b/ecs.php @@ -22,6 +22,7 @@ ]) ->withSkip([ '*/Source/*', + '*/SourcePhp81/*', '*/Fixture/*', '*/Expected/*', diff --git a/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php index 0f3aea38921..3a004302a46 100644 --- a/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php +++ b/rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/SourcePhp81/NotNumber.php @@ -7,11 +7,7 @@ #[\Attribute] final class NotNumber { - public function __construct( - $firstValue = null, - $secondValue = null, - $hey = null, - $hi = null - ) { + public function __construct($firstValue = null, $secondValue = null, $hey = null, $hi = null) + { } }