From 16238a7f8359a80f31791d38b9002f294ce0e753 Mon Sep 17 00:00:00 2001 From: Jean-Beru Date: Thu, 6 Aug 2026 17:46:29 +0200 Subject: [PATCH] Do not report a private method overriding a private trait method as unused A class member takes precedence over the member of the same name coming from a used trait, so a private method redeclared in the class is what the trait's own methods call. The rule sees those call sites only when the trait is part of the analysed files: analysing a project whose paths do not include its dependencies leaves ClassMethodsNode::getMethodCalls() with no call at all, and the class method is reported as unused. The canonical case is a Symfony application, where the framework recipe generates a Kernel redeclaring KernelTrait::getAllowedEnvs() while the trait lives in vendor/, outside of the analysed paths. Skip those methods. Nothing is left to distinguish an override that the trait calls from one it does not, so a redeclared private method the trait never calls is no longer reported either. Fixes phpstan/phpstan#12201 Assisted-by: Claude Code:claude-opus-5 --- .../DeadCode/UnusedPrivateMethodRule.php | 27 +++++++++++++++++++ .../DeadCode/UnusedPrivateMethodRuleTest.php | 10 +++++++ .../Rules/DeadCode/data/bug-12201-traits.php | 27 +++++++++++++++++++ .../PHPStan/Rules/DeadCode/data/bug-12201.php | 27 +++++++++++++++++++ 4 files changed, 91 insertions(+) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-12201-traits.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-12201.php diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 4a442bf489d..eb51ce964df 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -9,6 +9,7 @@ use PHPStan\DependencyInjection\ExtensionsCollection; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassMethodsNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Methods\AlwaysUsedMethodExtension; use PHPStan\Rules\Rule; @@ -70,6 +71,10 @@ public function processNode(Node $node, Scope $scope): array continue; } + if ($this->isOverridingPrivateTraitMethod($classReflection, $methodName)) { + continue; + } + $methodReflection = $classReflection->getNativeMethod($methodName); foreach ($this->extensions->getAll() as $extension) { if ($extension->isAlwaysUsed($methodReflection)) { @@ -203,4 +208,26 @@ public function processNode(Node $node, Scope $scope): array return $errors; } + /** + * A private method overriding a private method of a used trait is called from + * the trait's own methods. Those call sites are invisible when the trait is not + * part of the analysed files. + */ + private function isOverridingPrivateTraitMethod(ClassReflection $classReflection, string $methodName): bool + { + foreach ($classReflection->getTraits() as $trait) { + if (!$trait->hasNativeMethod($methodName)) { + continue; + } + + if (!$trait->getNativeMethod($methodName)->isPrivate()) { + continue; + } + + return true; + } + + return false; + } + } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index f57d00b9e5f..ccf290cc5f9 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -134,6 +134,16 @@ public function testBug11802(): void ]); } + public function testBug12201(): void + { + $this->analyse([__DIR__ . '/data/bug-12201.php'], [ + [ + 'Method Bug12201\AnotherKernel::doNothing() is unused.', + 24, + ], + ]); + } + public function testBug14880(): void { $this->analyse([__DIR__ . '/data/bug-14880.php'], []); diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12201-traits.php b/tests/PHPStan/Rules/DeadCode/data/bug-12201-traits.php new file mode 100644 index 00000000000..bcf859eae81 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12201-traits.php @@ -0,0 +1,27 @@ +getAllowedEnvs(); + } +} + +trait MicroKernelTrait +{ + use KernelTrait; +} diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-12201.php b/tests/PHPStan/Rules/DeadCode/data/bug-12201.php new file mode 100644 index 00000000000..cc655543a71 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-12201.php @@ -0,0 +1,27 @@ + + */ + private function getAllowedEnvs(): array + { + return ['prod', 'dev', 'test']; + } +} + +class AnotherKernel +{ + use MicroKernelTrait; + + private function doNothing(): void + { + } +}