From fc1584c92c0e1ace4db850088c7d794ce1ea1ee0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Mar 2026 03:44:23 +0000 Subject: [PATCH] Fix phpstan/phpstan#10690: skip unused private constant check for trait constants - Added ClassConstant wrapper class (similar to ClassMethod) that tracks isDeclaredInTrait - Updated ClassStatementsGatherer to wrap ClassConst nodes with trait context - Updated ClassConstantsNode with getClassConstants() method preserving backward compatibility - UnusedPrivateConstantRule now skips constants declared in traits - Added regression test in tests/PHPStan/Rules/DeadCode/data/bug-10690.php --- src/Node/ClassConstant.php | 30 +++++++++++++++++++ src/Node/ClassConstantsNode.php | 11 ++++++- src/Node/ClassStatementsGatherer.php | 6 ++-- .../DeadCode/UnusedPrivateConstantRule.php | 7 ++++- .../UnusedPrivateConstantRuleTest.php | 6 ++++ .../PHPStan/Rules/DeadCode/data/bug-10690.php | 29 ++++++++++++++++++ 6 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/Node/ClassConstant.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-10690.php diff --git a/src/Node/ClassConstant.php b/src/Node/ClassConstant.php new file mode 100644 index 0000000000..3b6ce13b24 --- /dev/null +++ b/src/Node/ClassConstant.php @@ -0,0 +1,30 @@ +node; + } + + public function isDeclaredInTrait(): bool + { + return $this->isDeclaredInTrait; + } + +} diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index fbc120199d..96c8e5c5b4 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -8,6 +8,7 @@ use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; use PHPStan\Reflection\ClassReflection; +use function array_map; /** * @api @@ -16,7 +17,7 @@ final class ClassConstantsNode extends NodeAbstract implements VirtualNode { /** - * @param ClassConst[] $constants + * @param ClassConstant[] $constants * @param ClassConstantFetch[] $fetches */ public function __construct(private ClassLike $class, private array $constants, private array $fetches, private ClassReflection $classReflection) @@ -33,6 +34,14 @@ public function getClass(): ClassLike * @return ClassConst[] */ public function getConstants(): array + { + return array_map(static fn (ClassConstant $constant) => $constant->getNode(), $this->constants); + } + + /** + * @return ClassConstant[] + */ + public function getClassConstants(): array { return $this->constants; } diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index a2bb6889d5..093c78090c 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -47,7 +47,7 @@ final class ClassStatementsGatherer /** @var array */ private array $propertyUsages = []; - /** @var Node\Stmt\ClassConst[] */ + /** @var ClassConstant[] */ private array $constants = []; /** @var ClassConstantFetch[] */ @@ -103,7 +103,7 @@ public function getPropertyUsages(): array } /** - * @return Node\Stmt\ClassConst[] + * @return ClassConstant[] */ public function getConstants(): array { @@ -165,7 +165,7 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof Node\Stmt\ClassConst) { - $this->constants[] = $node; + $this->constants[] = new ClassConstant($node, $scope->isInTrait()); return; } if ($node instanceof MethodCall || $node instanceof StaticCall) { diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 6e017873d2..78a2031ec7 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -38,11 +38,16 @@ public function processNode(Node $node, Scope $scope): array $classType = new ObjectType($classReflection->getName(), classReflection: $classReflection); $constants = []; - foreach ($node->getConstants() as $constant) { + foreach ($node->getClassConstants() as $classConstant) { + $constant = $classConstant->getNode(); if (!$constant->isPrivate()) { continue; } + if ($classConstant->isDeclaredInTrait()) { + continue; + } + foreach ($constant->consts as $const) { $constantName = $const->name->toString(); diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index 79e15a843c..3f66224c3d 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -88,6 +88,12 @@ public function testBug9765(): void $this->analyse([__DIR__ . '/data/bug-9765.php'], []); } + #[RequiresPhp('>= 8.2')] + public function testBug10690(): void + { + $this->analyse([__DIR__ . '/data/bug-10690.php'], []); + } + #[RequiresPhp('>= 8.3')] public function testDynamicConstantFetch(): void { diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-10690.php b/tests/PHPStan/Rules/DeadCode/data/bug-10690.php new file mode 100644 index 0000000000..ecea417378 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-10690.php @@ -0,0 +1,29 @@ += 8.2 + +declare(strict_types = 1); + +namespace Bug10690; + +trait MyTrait +{ + private const AAA='aaa'; + private const BBB='bbb'; +} + +final class First +{ + use MyTrait; + + function a(): string { + return self::AAA.self::BBB; + } +} + +final class SecondConsumer +{ + use MyTrait; + + function b(): string { + return self::AAA; + } +}