From 4decd584983249b8d89cbef93556c973506428ed Mon Sep 17 00:00:00 2001 From: Philip Norton Date: Thu, 5 Nov 2026 10:43:16 +0200 Subject: [PATCH 1/2] [NodeTypeResolver] Set scope on the class name of a first-class callable processCallLike() returned early for every first-class callable, which left the class name of self::method(...) and static::method(...) without a scope. NameTypeResolver needs that scope to resolve self and static, so it returned an ObjectType for a class literally named "self" or "static". RemoveUnusedPrivateMethodParameterRector could then not match the callable to its class, missed the isFirstClassCallable() guard and removed a parameter from a method whose arity the receiving code controls. The early return only has to protect getArgs(), so it moves back below the scope assignments, where it was before #8793. Co-Authored-By: Claude Opus 5.5 --- .../skip_with_first_class_callable_self.php.inc | 16 ++++++++++++++++ ...lass_callable_self_in_instance_method.php.inc | 16 ++++++++++++++++ ...skip_with_first_class_callable_static.php.inc | 16 ++++++++++++++++ .../PHPStan/Scope/PHPStanNodeScopeResolver.php | 8 ++++---- 4 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_self.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_self_in_instance_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_static.php.inc diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_self.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_self.php.inc new file mode 100644 index 00000000000..b8bcb2a56fe --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_first_class_callable_self.php.inc @@ -0,0 +1,16 @@ +isFirstClassCallable()) { - return; - } - if ($callLike instanceof StaticCall) { $callLike->class->setAttribute(AttributeKey::SCOPE, $mutatingScope); $callLike->name->setAttribute(AttributeKey::SCOPE, $mutatingScope); @@ -398,6 +394,10 @@ private function processCallLike(CallLike $callLike, MutatingScope $mutatingScop $callLike->class->setAttribute(AttributeKey::SCOPE, $mutatingScope); } + if ($callLike->isFirstClassCallable()) { + return; + } + foreach ($callLike->getArgs() as $arg) { $arg->value->setAttribute(AttributeKey::SCOPE, $mutatingScope); } From 212a6de2b7348dc889b00d2bc30d5d65b03c29af Mon Sep 17 00:00:00 2001 From: Philip Norton Date: Thu, 5 Nov 2026 10:43:16 +0200 Subject: [PATCH 2/2] [DeadCode] Skip private method used as array callable on removing unused parameter The code receiving [self::class, 'method'], [static::class, 'method'] or [$this, 'method'] decides how many arguments it passes, the same as with a first-class callable. PrivateMethodParamRemover never looked at arrays, so it removed the parameter. It now keeps the parameter when an array in the class names the method, or when ArrayCallableMethodMatcher cannot tell which method the array names. Co-Authored-By: Claude Opus 5.5 --- ...with_array_callable_of_other_class.php.inc | 41 +++++++++++++++++++ ...with_array_callable_dynamic_method.php.inc | 16 ++++++++ ...array_callable_in_property_default.php.inc | 13 ++++++ ...allable_required_constructor_param.php.inc | 20 +++++++++ ...kip_with_array_callable_self_class.php.inc | 16 ++++++++ ...p_with_array_callable_static_class.php.inc | 16 ++++++++ .../skip_with_array_callable_this.php.inc | 16 ++++++++ .../PrivateMethodParamRemover.php | 38 +++++++++++++++++ 8 files changed, 176 insertions(+) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/remove_with_array_callable_of_other_class.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_dynamic_method.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_in_property_default.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_required_constructor_param.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_self_class.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_static_class.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_this.php.inc diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/remove_with_array_callable_of_other_class.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/remove_with_array_callable_of_other_class.php.inc new file mode 100644 index 00000000000..8c0fda45193 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/remove_with_array_callable_of_other_class.php.inc @@ -0,0 +1,41 @@ +format('unused', 'value'); + + return [\DateTime::class, 'format']; + } + + private function format(string $unused, string $value): string + { + return $value; + } +} + +?> +----- +format('value'); + + return [\DateTime::class, 'format']; + } + + private function format(string $value): string + { + return $value; + } +} + +?> diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_dynamic_method.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_dynamic_method.php.inc new file mode 100644 index 00000000000..1b6a4d33298 --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUnusedPrivateMethodParameterRector/Fixture/skip_with_array_callable_dynamic_method.php.inc @@ -0,0 +1,16 @@ +isUsedAsArrayCallable($class, $classMethod)) { + return false; + } + $keysArg = array_keys($paramsToRemove); $classObjectType = new ObjectType((string) $this->nodeNameResolver->getName($class)); @@ -112,6 +123,33 @@ private function removeCallerArgs(Class_ $class, ClassMethod $classMethod, array return true; } + private function isUsedAsArrayCallable(Class_ $class, ClassMethod $classMethod): bool + { + $scope = ScopeFetcher::fetch($class); + $methodName = $this->nodeNameResolver->getName($classMethod); + + /** @var Array_[] $arrays */ + $arrays = $this->betterNodeFinder->findInstanceOf($class, Array_::class); + + foreach ($arrays as $array) { + $arrayCallable = $this->arrayCallableMethodMatcher->match($array, $scope, $methodName); + if ($arrayCallable instanceof ArrayCallableDynamicMethod) { + return true; + } + + if (! $arrayCallable instanceof ArrayCallable) { + continue; + } + + if ($this->nodeNameResolver->isName($class, $arrayCallable->getClass()) + && $this->nodeNameResolver->isName($classMethod, $arrayCallable->getMethod())) { + return true; + } + } + + return false; + } + /** * @param int[] $keysArg */