From 5c6aa81d9f899eb1ad4b941a261e61c888d77273 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 20 Oct 2025 00:10:52 +0200 Subject: [PATCH 1/2] Report unused protected when class is final --- .../DeadCode/UnusedPrivateConstantRule.php | 3 +- .../DeadCode/UnusedPrivateMethodRule.php | 7 ++- .../DeadCode/UnusedPrivatePropertyRule.php | 3 +- .../UnusedPrivateConstantRuleTest.php | 16 +++++++ .../DeadCode/UnusedPrivateMethodRuleTest.php | 14 ++++++ .../UnusedPrivatePropertyRuleTest.php | 18 ++++++++ .../data/unused-protected-constant.php | 35 +++++++++++++++ .../DeadCode/data/unused-protected-method.php | 40 +++++++++++++++++ .../data/unused-protected-property.php | 45 +++++++++++++++++++ 9 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/unused-protected-constant.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/unused-protected-method.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/unused-protected-property.php diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 6e017873d2..c238a6b95b 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -35,11 +35,12 @@ public function processNode(Node $node, Scope $scope): array } $classReflection = $node->getClassReflection(); + $isClassFinal = $classReflection->isFinalByKeyword(); $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constants = []; foreach ($node->getConstants() as $constant) { - if (!$constant->isPrivate()) { + if ($constant->isPublic() || ($constant->isProtected() && !$isClassFinal)) { continue; } diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index 2c1ab6ce64..ba3ea9364a 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -40,6 +40,7 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); + $isClassFinal = $classReflection->isFinalByKeyword(); $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constructor = null; if ($classReflection->hasConstructor()) { @@ -48,13 +49,15 @@ public function processNode(Node $node, Scope $scope): array $methods = []; foreach ($node->getMethods() as $method) { - if (!$method->getNode()->isPrivate()) { + $methodNode = $method->getNode(); + if ($methodNode->isPublic() || ($methodNode->isProtected() && !$isClassFinal)) { continue; } if ($method->isDeclaredInTrait()) { continue; } - $methodName = $method->getNode()->name->toString(); + + $methodName = $methodNode->name->toString(); if ($constructor !== null && $constructor->getName() === $methodName) { continue; } diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index 7d2ccc7328..d4f948e089 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -56,10 +56,11 @@ public function processNode(Node $node, Scope $scope): array return []; } $classReflection = $node->getClassReflection(); + $isClassFinal = $classReflection->isFinalByKeyword(); $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $properties = []; foreach ($node->getProperties() as $property) { - if (!$property->isPrivate()) { + if ($property->isPublic() || ($property->isProtected() && !$isClassFinal)) { continue; } if ($property->isDeclaredInTrait()) { diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index 79e15a843c..99e76f6519 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -49,6 +49,22 @@ public function testRule(): void ]); } + public function testProtected(): void + { + $this->analyse([__DIR__ . '/data/unused-protected-constant.php'], [ + [ + 'Constant UnusedProtectedConstant\Bar::BAR_CONST is unused.', + 26, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + [ + 'Constant UnusedProtectedConstant\Bar::BAZ_CONST is unused.', + 28, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + public function testBug5651(): void { $this->analyse([__DIR__ . '/data/bug-5651.php'], []); diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index 093d372358..61da514fa2 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -62,6 +62,20 @@ public function testRule(): void ]); } + public function testProtected(): void + { + $this->analyse([__DIR__ . '/data/unused-protected-method.php'], [ + [ + 'Method UnusedProtectedMethod\Bar::unused1() is unused.', + 30, + ], + [ + 'Method UnusedProtectedMethod\Bar::unused2() is unused.', + 35, + ], + ]); + } + public function testBug3630(): void { $this->analyse([__DIR__ . '/data/bug-3630.php'], []); diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 199e22d1a2..21cac2bac4 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -186,6 +186,24 @@ public function testTrait(): void $this->analyse([__DIR__ . '/data/private-property-trait.php'], []); } + public function testProtected(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + $this->analyse([__DIR__ . '/data/unused-protected-property.php'], [ + [ + 'Property UnusedProtectedProperty\Bar::$bar is unused.', + 31, + 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + ], + [ + 'Property UnusedProtectedProperty\Bar::$baz is unused.', + 33, + 'See: https://phpstan.org/developing-extensions/always-read-written-properties', + ], + ]); + } + public function testBug3636(): void { $this->alwaysWrittenTags = []; diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-protected-constant.php b/tests/PHPStan/Rules/DeadCode/data/unused-protected-constant.php new file mode 100644 index 0000000000..c59f95c744 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-protected-constant.php @@ -0,0 +1,35 @@ += 8.1 + +namespace UnusedProtectedConstant; + +class Foo +{ + + protected const FOO_CONST = 1; + + protected const BAR_CONST = 2; + + final protected const BAZ_CONST = 2; + + public function doFoo() + { + echo self::FOO_CONST; + } + +} + +final class Bar +{ + + protected const FOO_CONST = 1; + + protected const BAR_CONST = 2; + + final protected const BAZ_CONST = 2; + + public function doFoo() + { + echo self::FOO_CONST; + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-protected-method.php b/tests/PHPStan/Rules/DeadCode/data/unused-protected-method.php new file mode 100644 index 0000000000..28ec116d14 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-protected-method.php @@ -0,0 +1,40 @@ +used1(); + } + + final protected function unused2() + { + $this->used1(); + } + +} + +final class Bar +{ + + protected function used1() + { + } + + protected function unused1() + { + $this->used1(); + } + + final protected function unused2() + { + $this->used1(); + } + +} diff --git a/tests/PHPStan/Rules/DeadCode/data/unused-protected-property.php b/tests/PHPStan/Rules/DeadCode/data/unused-protected-property.php new file mode 100644 index 0000000000..2baacfd585 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/unused-protected-property.php @@ -0,0 +1,45 @@ += 8.4 + +namespace UnusedProtectedProperty; + +class Foo +{ + + protected $foo; + + protected string $bar; + + final protected string $baz; + + public function __construct() + { + $this->foo = 1; + } + + public function getFoo() + { + return $this->foo; + } + +} + +final class Bar +{ + + protected $foo; + + protected string $bar; + + final protected string $baz; + + public function __construct() + { + $this->foo = 1; + } + + public function getFoo() + { + return $this->foo; + } + +} From 2a77e22675bf528009a6aef60cb8b78c3c7593cc Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Mon, 20 Oct 2025 09:34:00 +0200 Subject: [PATCH 2/2] Try --- src/Rules/DeadCode/UnusedPrivateMethodRule.php | 4 ++++ src/Rules/DeadCode/UnusedPrivatePropertyRule.php | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/Rules/DeadCode/UnusedPrivateMethodRule.php b/src/Rules/DeadCode/UnusedPrivateMethodRule.php index ba3ea9364a..c3cc6e3d7d 100644 --- a/src/Rules/DeadCode/UnusedPrivateMethodRule.php +++ b/src/Rules/DeadCode/UnusedPrivateMethodRule.php @@ -66,6 +66,10 @@ public function processNode(Node $node, Scope $scope): array } $methodReflection = $classReflection->getNativeMethod($methodName); + if ($methodReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } + foreach ($this->extensionProvider->getExtensions() as $extension) { if ($extension->isAlwaysUsed($methodReflection)) { continue 2; diff --git a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php index d4f948e089..ba510d1e52 100644 --- a/src/Rules/DeadCode/UnusedPrivatePropertyRule.php +++ b/src/Rules/DeadCode/UnusedPrivatePropertyRule.php @@ -97,6 +97,9 @@ public function processNode(Node $node, Scope $scope): array } $propertyReflection = $classReflection->getNativeProperty($propertyName); + if ($propertyReflection->getDeclaringClass()->getName() !== $classReflection->getName()) { + continue; + } foreach ($this->extensionProvider->getExtensions() as $extension) { if ($alwaysRead && $alwaysWritten) {