From afc0737e1e713e267e8f8436d42ef637bbcb323e Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Sat, 16 May 2026 12:35:11 +0000 Subject: [PATCH 1/3] Scan PHPDoc type nodes for class constant references in `UnusedPrivateConstantRule` - Add PHPDoc scanning to UnusedPrivateConstantRule to detect constant references in value-of<>, key-of<>, int-mask-of<>, and const type notation (self::CONST, self::CONST*) - Walk the raw PhpDocNode AST recursively to find ConstFetchNode instances that reference the class's own constants - Scan class-level, method-level, and property-level PHPDocs - Handle wildcard patterns (self::FLAG_*) with regex matching - Update testBug9039 which now correctly recognizes self::* in @template-extends as constant usage --- .../DeadCode/UnusedPrivateConstantRule.php | 119 ++++++++++++++ .../Analyser/AnalyserIntegrationTest.php | 3 +- .../UnusedPrivateConstantRuleTest.php | 11 ++ .../PHPStan/Rules/DeadCode/data/bug-6415.php | 149 ++++++++++++++++++ 4 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-6415.php diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 6e017873d20..ede7657dfed 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -6,11 +6,22 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassConstantsNode; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Node as PhpDocNode; +use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; +use function array_keys; +use function get_object_vars; +use function is_array; +use function preg_match; +use function preg_quote; use function sprintf; +use function str_contains; +use function str_replace; +use function strtolower; /** * @implements Rule @@ -93,6 +104,10 @@ public function processNode(Node $node, Scope $scope): array unset($constants[$fetchNode->name->toString()]); } + if ($constants !== []) { + $this->removePhpDocReferencedConstants($classReflection, $constants); + } + $errors = []; foreach ($constants as $constantName => $constantNode) { $errors[] = RuleErrorBuilder::message(sprintf('Constant %s::%s is unused.', $classReflection->getDisplayName(), $constantName)) @@ -105,4 +120,108 @@ public function processNode(Node $node, Scope $scope): array return $errors; } + /** + * @param array $constants + */ + private function removePhpDocReferencedConstants(ClassReflection $classReflection, array &$constants): void + { + $className = $classReflection->getName(); + + $phpDocBlocks = []; + + $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); + if ($resolvedPhpDoc !== null) { + $phpDocBlocks[] = $resolvedPhpDoc; + } + + foreach ($classReflection->getNativeReflection()->getMethods() as $method) { + if ($method->getDeclaringClass()->getName() !== $className) { + continue; + } + if (!$classReflection->hasNativeMethod($method->getName())) { + continue; + } + $methodReflection = $classReflection->getNativeMethod($method->getName()); + $methodPhpDoc = $methodReflection->getResolvedPhpDoc(); + if ($methodPhpDoc === null) { + continue; + } + + $phpDocBlocks[] = $methodPhpDoc; + } + + foreach ($classReflection->getNativeReflection()->getProperties() as $property) { + if ($property->getDeclaringClass()->getName() !== $className) { + continue; + } + if (!$classReflection->hasNativeProperty($property->getName())) { + continue; + } + $propertyReflection = $classReflection->getNativeProperty($property->getName()); + $propertyPhpDoc = $propertyReflection->getResolvedPhpDoc(); + if ($propertyPhpDoc === null) { + continue; + } + + $phpDocBlocks[] = $propertyPhpDoc; + } + + foreach ($phpDocBlocks as $phpDocBlock) { + foreach ($phpDocBlock->getPhpDocNodes() as $phpDocNode) { + $this->findConstFetchReferences($phpDocNode, $className, $constants); + } + if ($constants === []) { + return; + } + } + } + + /** + * @param array $constants + */ + private function findConstFetchReferences(PhpDocNode $phpDocNode, string $className, array &$constants): void + { + if ($phpDocNode instanceof ConstFetchNode) { + if ($phpDocNode->className !== '' && $this->isSameClass($phpDocNode->className, $className)) { + $name = $phpDocNode->name; + if (str_contains($name, '*')) { + $pattern = '{^' . str_replace('\\*', '.*', preg_quote($name, '{')) . '$}D'; + foreach (array_keys($constants) as $constantName) { + if (preg_match($pattern, $constantName) !== 1) { + continue; + } + + unset($constants[$constantName]); + } + } else { + unset($constants[$name]); + } + } + return; + } + + foreach (get_object_vars($phpDocNode) as $prop) { + if ($prop instanceof PhpDocNode) { + $this->findConstFetchReferences($prop, $className, $constants); + } elseif (is_array($prop)) { + foreach ($prop as $item) { + if (!($item instanceof PhpDocNode)) { + continue; + } + + $this->findConstFetchReferences($item, $className, $constants); + } + } + if ($constants === []) { + return; + } + } + } + + private function isSameClass(string $fetchedClassName, string $className): bool + { + $lower = strtolower($fetchedClassName); + return $lower === 'self' || $lower === 'static' || strtolower($className) === strtolower($fetchedClassName); + } + } diff --git a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php index 27b9f44c3de..67b605bd08e 100644 --- a/tests/PHPStan/Analyser/AnalyserIntegrationTest.php +++ b/tests/PHPStan/Analyser/AnalyserIntegrationTest.php @@ -1200,8 +1200,7 @@ public function testBug9039(): void { // crash $errors = $this->runAnalyse(__DIR__ . '/data/bug-9039.php'); - $this->assertCount(1, $errors); - $this->assertSame('Constant Bug9039\Test::RULES is unused.', $errors[0]->getMessage()); + $this->assertNoErrors($errors); } #[RequiresPhp('>= 8.0.0')] diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php index e990fae8340..8de21153a0b 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateConstantRuleTest.php @@ -100,4 +100,15 @@ public function testDynamicConstantFetch(): void ]); } + public function testBug6415(): void + { + $this->analyse([__DIR__ . '/data/bug-6415.php'], [ + [ + 'Constant Bug6415\MixedUsage::ACTUALLY_UNUSED is unused.', + 141, + 'See: https://phpstan.org/developing-extensions/always-used-class-constants', + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-6415.php b/tests/PHPStan/Rules/DeadCode/data/bug-6415.php new file mode 100644 index 00000000000..b76c77f6dad --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6415.php @@ -0,0 +1,149 @@ + 1, + 'b' => 2, + 'c' => 3, + ]; + + /** @return value-of */ + public function getValueOf(): int + { + return 1; + } + +} + +class Bar +{ + + private const SOME_MAP = [ + 'x' => 'hello', + 'y' => 'world', + ]; + + /** @return key-of */ + public function getKeyOf(): string + { + return 'x'; + } + +} + +class Baz +{ + + private const STATUS_ACTIVE = 'active'; + private const STATUS_INACTIVE = 'inactive'; + + private const STATUSES = [ + self::STATUS_ACTIVE, + self::STATUS_INACTIVE, + ]; + + /** @return value-of */ + public function getStatus(): string + { + return self::STATUS_ACTIVE; + } + +} + +class PropertyPhpDoc +{ + + private const ALLOWED_KEYS = ['foo', 'bar', 'baz']; + + /** @var key-of */ + private int $selectedIndex = 0; + +} + +class IntMaskOf +{ + + private const FLAG_A = 1; + private const FLAG_B = 2; + private const FLAG_C = 4; + + /** @return int-mask-of */ + public function getFlags(): int + { + return self::FLAG_A | self::FLAG_B; + } + +} + +class ConstTypeNotation +{ + + private const MODE_READ = 'read'; + private const MODE_WRITE = 'write'; + + /** @param self::MODE_* $mode */ + public function setMode(string $mode): void + { + } + +} + +/** + * @phpstan-type AllowedValue value-of + */ +class TypeAlias +{ + + private const ALLOWED_VALUES = ['a', 'b', 'c']; + + /** @return AllowedValue */ + public function getValue(): string + { + return 'a'; + } + +} + +class ParamType +{ + + private const FORMATS = ['json', 'xml', 'csv']; + + /** @param value-of $format */ + public function export(string $format): void + { + } + +} + +class AssertType +{ + + private const VALID_TYPES = ['foo', 'bar']; + + /** + * @phpstan-assert value-of $value + */ + public function assertValid(string $value): void + { + } + +} + +class MixedUsage +{ + + private const USED_IN_PHPDOC = ['a', 'b']; + private const ACTUALLY_UNUSED = 'unused'; + + /** @return value-of */ + public function getValue(): string + { + return 'a'; + } + +} From 4fc180f20f1fbcf250b8684a283f6ede611cbdd4 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 13:21:59 +0000 Subject: [PATCH 2/3] Move PHPDoc constant reference scanning from rule to ClassConstantsNode Rather than checking PHPDoc constant references directly in UnusedPrivateConstantRule, move the scanning logic to ClassConstantsNode where it is lazily computed via getPhpDocFetches(). This keeps the rule simple and makes PHPDoc constant reference data available to any rule that processes ClassConstantsNode. Introduces PhpDocClassConstantReference value object to represent PHPDoc-based constant references (className + constantName). Co-Authored-By: Claude Opus 4.6 --- src/Node/ClassConstantsNode.php | 95 ++++++++++++++++++ .../Constant/PhpDocClassConstantReference.php | 22 +++++ .../DeadCode/UnusedPrivateConstantRule.php | 96 +++---------------- 3 files changed, 130 insertions(+), 83 deletions(-) create mode 100644 src/Node/Constant/PhpDocClassConstantReference.php diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index fbc120199d1..c8ca588baad 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -7,7 +7,12 @@ use PhpParser\Node\Stmt\ClassLike; use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Node\Constant\PhpDocClassConstantReference; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Node as PhpDocNode; use PHPStan\Reflection\ClassReflection; +use function get_object_vars; +use function is_array; /** * @api @@ -15,6 +20,9 @@ final class ClassConstantsNode extends NodeAbstract implements VirtualNode { + /** @var PhpDocClassConstantReference[]|null */ + private ?array $phpDocFetches = null; + /** * @param ClassConst[] $constants * @param ClassConstantFetch[] $fetches @@ -45,6 +53,66 @@ public function getFetches(): array return $this->fetches; } + /** + * @return PhpDocClassConstantReference[] + */ + public function getPhpDocFetches(): array + { + if ($this->phpDocFetches !== null) { + return $this->phpDocFetches; + } + + $result = []; + $className = $this->classReflection->getName(); + + $resolvedPhpDoc = $this->classReflection->getResolvedPhpDoc(); + if ($resolvedPhpDoc !== null) { + foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpDocNode) { + $this->collectPhpDocConstantFetches($phpDocNode, $result); + } + } + + foreach ($this->classReflection->getNativeReflection()->getMethods() as $method) { + if ($method->getDeclaringClass()->getName() !== $className) { + continue; + } + if (!$this->classReflection->hasNativeMethod($method->getName())) { + continue; + } + $methodReflection = $this->classReflection->getNativeMethod($method->getName()); + $methodPhpDoc = $methodReflection->getResolvedPhpDoc(); + if ($methodPhpDoc === null) { + continue; + } + + foreach ($methodPhpDoc->getPhpDocNodes() as $phpDocNode) { + $this->collectPhpDocConstantFetches($phpDocNode, $result); + } + } + + foreach ($this->classReflection->getNativeReflection()->getProperties() as $property) { + if ($property->getDeclaringClass()->getName() !== $className) { + continue; + } + if (!$this->classReflection->hasNativeProperty($property->getName())) { + continue; + } + $propertyReflection = $this->classReflection->getNativeProperty($property->getName()); + $propertyPhpDoc = $propertyReflection->getResolvedPhpDoc(); + if ($propertyPhpDoc === null) { + continue; + } + + foreach ($propertyPhpDoc->getPhpDocNodes() as $phpDocNode) { + $this->collectPhpDocConstantFetches($phpDocNode, $result); + } + } + + $this->phpDocFetches = $result; + + return $result; + } + #[Override] public function getType(): string { @@ -65,4 +133,31 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } + /** + * @param PhpDocClassConstantReference[] $result + */ + private function collectPhpDocConstantFetches(PhpDocNode $phpDocNode, array &$result): void + { + if ($phpDocNode instanceof ConstFetchNode) { + if ($phpDocNode->className !== '') { + $result[] = new PhpDocClassConstantReference($phpDocNode->className, $phpDocNode->name); + } + return; + } + + foreach (get_object_vars($phpDocNode) as $prop) { + if ($prop instanceof PhpDocNode) { + $this->collectPhpDocConstantFetches($prop, $result); + } elseif (is_array($prop)) { + foreach ($prop as $item) { + if (!($item instanceof PhpDocNode)) { + continue; + } + + $this->collectPhpDocConstantFetches($item, $result); + } + } + } + } + } diff --git a/src/Node/Constant/PhpDocClassConstantReference.php b/src/Node/Constant/PhpDocClassConstantReference.php new file mode 100644 index 00000000000..ebe369b53b6 --- /dev/null +++ b/src/Node/Constant/PhpDocClassConstantReference.php @@ -0,0 +1,22 @@ +className; + } + + public function getConstantName(): string + { + return $this->constantName; + } + +} diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index ede7657dfed..64c50b55a6b 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -6,16 +6,12 @@ use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\RegisteredRule; use PHPStan\Node\ClassConstantsNode; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; -use PHPStan\PhpDocParser\Ast\Node as PhpDocNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Rules\Constants\AlwaysUsedClassConstantsExtensionProvider; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\ObjectType; use function array_keys; -use function get_object_vars; -use function is_array; use function preg_match; use function preg_quote; use function sprintf; @@ -105,7 +101,7 @@ public function processNode(Node $node, Scope $scope): array } if ($constants !== []) { - $this->removePhpDocReferencedConstants($classReflection, $constants); + $this->removePhpDocReferencedConstants($node, $classReflection, $constants); } $errors = []; @@ -123,95 +119,29 @@ public function processNode(Node $node, Scope $scope): array /** * @param array $constants */ - private function removePhpDocReferencedConstants(ClassReflection $classReflection, array &$constants): void + private function removePhpDocReferencedConstants(ClassConstantsNode $node, ClassReflection $classReflection, array &$constants): void { $className = $classReflection->getName(); - $phpDocBlocks = []; - - $resolvedPhpDoc = $classReflection->getResolvedPhpDoc(); - if ($resolvedPhpDoc !== null) { - $phpDocBlocks[] = $resolvedPhpDoc; - } - - foreach ($classReflection->getNativeReflection()->getMethods() as $method) { - if ($method->getDeclaringClass()->getName() !== $className) { - continue; - } - if (!$classReflection->hasNativeMethod($method->getName())) { - continue; - } - $methodReflection = $classReflection->getNativeMethod($method->getName()); - $methodPhpDoc = $methodReflection->getResolvedPhpDoc(); - if ($methodPhpDoc === null) { - continue; - } - - $phpDocBlocks[] = $methodPhpDoc; - } - - foreach ($classReflection->getNativeReflection()->getProperties() as $property) { - if ($property->getDeclaringClass()->getName() !== $className) { - continue; - } - if (!$classReflection->hasNativeProperty($property->getName())) { + foreach ($node->getPhpDocFetches() as $phpDocFetch) { + if (!$this->isSameClass($phpDocFetch->getClassName(), $className)) { continue; } - $propertyReflection = $classReflection->getNativeProperty($property->getName()); - $propertyPhpDoc = $propertyReflection->getResolvedPhpDoc(); - if ($propertyPhpDoc === null) { - continue; - } - - $phpDocBlocks[] = $propertyPhpDoc; - } - - foreach ($phpDocBlocks as $phpDocBlock) { - foreach ($phpDocBlock->getPhpDocNodes() as $phpDocNode) { - $this->findConstFetchReferences($phpDocNode, $className, $constants); - } - if ($constants === []) { - return; - } - } - } - - /** - * @param array $constants - */ - private function findConstFetchReferences(PhpDocNode $phpDocNode, string $className, array &$constants): void - { - if ($phpDocNode instanceof ConstFetchNode) { - if ($phpDocNode->className !== '' && $this->isSameClass($phpDocNode->className, $className)) { - $name = $phpDocNode->name; - if (str_contains($name, '*')) { - $pattern = '{^' . str_replace('\\*', '.*', preg_quote($name, '{')) . '$}D'; - foreach (array_keys($constants) as $constantName) { - if (preg_match($pattern, $constantName) !== 1) { - continue; - } - - unset($constants[$constantName]); - } - } else { - unset($constants[$name]); - } - } - return; - } - foreach (get_object_vars($phpDocNode) as $prop) { - if ($prop instanceof PhpDocNode) { - $this->findConstFetchReferences($prop, $className, $constants); - } elseif (is_array($prop)) { - foreach ($prop as $item) { - if (!($item instanceof PhpDocNode)) { + $name = $phpDocFetch->getConstantName(); + if (str_contains($name, '*')) { + $pattern = '{^' . str_replace('\\*', '.*', preg_quote($name, '{')) . '$}D'; + foreach (array_keys($constants) as $constantName) { + if (preg_match($pattern, $constantName) !== 1) { continue; } - $this->findConstFetchReferences($item, $className, $constants); + unset($constants[$constantName]); } + } else { + unset($constants[$name]); } + if ($constants === []) { return; } From 94a8cffa614635d2c60bd14a9fd64cad33aee920 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 16 May 2026 13:48:50 +0000 Subject: [PATCH 3/3] Move PHPDoc constant reference resolution to ResolvedPhpDocBlock using NameScope Use NameScope::resolveStringName() to properly resolve class names in PHPDoc constant references instead of manual string comparison. This addresses the review feedback to access PHPDocs through the proper abstraction layer. - Add getClassConstantReferences() on ResolvedPhpDocBlock that walks PHPDoc AST nodes for ConstFetchNode and resolves class names using the block's NameScope (handles self/static, use aliases, namespaces) - In merge(), eagerly compute references from the child block (which has a valid NameScope) so merged blocks still carry the data - Simplify ClassConstantsNode::getPhpDocFetches() to delegate to ResolvedPhpDocBlock::getClassConstantReferences() - Remove isSameClass() from UnusedPrivateConstantRule - compare resolved class names directly Co-Authored-By: Claude Opus 4.6 --- src/Node/ClassConstantsNode.php | 43 ++--------- src/PhpDoc/ResolvedPhpDocBlock.php | 71 +++++++++++++++++++ .../DeadCode/UnusedPrivateConstantRule.php | 9 +-- 3 files changed, 78 insertions(+), 45 deletions(-) diff --git a/src/Node/ClassConstantsNode.php b/src/Node/ClassConstantsNode.php index c8ca588baad..f896e0a74a3 100644 --- a/src/Node/ClassConstantsNode.php +++ b/src/Node/ClassConstantsNode.php @@ -8,11 +8,7 @@ use PhpParser\NodeAbstract; use PHPStan\Node\Constant\ClassConstantFetch; use PHPStan\Node\Constant\PhpDocClassConstantReference; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; -use PHPStan\PhpDocParser\Ast\Node as PhpDocNode; use PHPStan\Reflection\ClassReflection; -use function get_object_vars; -use function is_array; /** * @api @@ -67,8 +63,8 @@ public function getPhpDocFetches(): array $resolvedPhpDoc = $this->classReflection->getResolvedPhpDoc(); if ($resolvedPhpDoc !== null) { - foreach ($resolvedPhpDoc->getPhpDocNodes() as $phpDocNode) { - $this->collectPhpDocConstantFetches($phpDocNode, $result); + foreach ($resolvedPhpDoc->getClassConstantReferences() as $reference) { + $result[] = $reference; } } @@ -85,8 +81,8 @@ public function getPhpDocFetches(): array continue; } - foreach ($methodPhpDoc->getPhpDocNodes() as $phpDocNode) { - $this->collectPhpDocConstantFetches($phpDocNode, $result); + foreach ($methodPhpDoc->getClassConstantReferences() as $reference) { + $result[] = $reference; } } @@ -103,8 +99,8 @@ public function getPhpDocFetches(): array continue; } - foreach ($propertyPhpDoc->getPhpDocNodes() as $phpDocNode) { - $this->collectPhpDocConstantFetches($phpDocNode, $result); + foreach ($propertyPhpDoc->getClassConstantReferences() as $reference) { + $result[] = $reference; } } @@ -133,31 +129,4 @@ public function getClassReflection(): ClassReflection return $this->classReflection; } - /** - * @param PhpDocClassConstantReference[] $result - */ - private function collectPhpDocConstantFetches(PhpDocNode $phpDocNode, array &$result): void - { - if ($phpDocNode instanceof ConstFetchNode) { - if ($phpDocNode->className !== '') { - $result[] = new PhpDocClassConstantReference($phpDocNode->className, $phpDocNode->name); - } - return; - } - - foreach (get_object_vars($phpDocNode) as $prop) { - if ($prop instanceof PhpDocNode) { - $this->collectPhpDocConstantFetches($prop, $result); - } elseif (is_array($prop)) { - foreach ($prop as $item) { - if (!($item instanceof PhpDocNode)) { - continue; - } - - $this->collectPhpDocConstantFetches($item, $result); - } - } - } - } - } diff --git a/src/PhpDoc/ResolvedPhpDocBlock.php b/src/PhpDoc/ResolvedPhpDocBlock.php index be6da4fc46d..509cd300561 100644 --- a/src/PhpDoc/ResolvedPhpDocBlock.php +++ b/src/PhpDoc/ResolvedPhpDocBlock.php @@ -3,6 +3,7 @@ namespace PHPStan\PhpDoc; use PHPStan\Analyser\NameScope; +use PHPStan\Node\Constant\PhpDocClassConstantReference; use PHPStan\PhpDoc\Tag\AssertTag; use PHPStan\PhpDoc\Tag\DeprecatedTag; use PHPStan\PhpDoc\Tag\ExtendsTag; @@ -25,6 +26,8 @@ use PHPStan\PhpDoc\Tag\TypedTag; use PHPStan\PhpDoc\Tag\UsesTag; use PHPStan\PhpDoc\Tag\VarTag; +use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; +use PHPStan\PhpDocParser\Ast\Node as PhpDocAstNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\ReflectionProvider; @@ -38,7 +41,11 @@ use function array_key_exists; use function array_map; use function count; +use function get_object_vars; +use function in_array; +use function is_array; use function is_bool; +use function strtolower; use function substr; /** @@ -153,6 +160,9 @@ final class ResolvedPhpDocBlock private ?bool $acceptsNamedArguments = null; + /** @var PhpDocClassConstantReference[]|false */ + private array|false $classConstantReferences = false; + private function __construct() { } @@ -244,6 +254,7 @@ public static function createEmpty(): self $self->isAllowedPrivateMutation = false; $self->hasConsistentConstructor = false; $self->acceptsNamedArguments = true; + $self->classConstantReferences = []; return $self; } @@ -300,6 +311,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi $result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation(); $result->hasConsistentConstructor = $this->hasConsistentConstructor(); $result->acceptsNamedArguments = $acceptsNamedArguments; + $result->classConstantReferences = $this->getClassConstantReferences(); return $result; } @@ -1148,4 +1160,63 @@ private static function resolveTemplateTypeInTag( return $tag->withType($type); } + /** + * @return PhpDocClassConstantReference[] + */ + public function getClassConstantReferences(): array + { + if ($this->classConstantReferences !== false) { + return $this->classConstantReferences; + } + + $nameScope = $this->nameScope; + if ($nameScope === null) { + $this->classConstantReferences = []; + return $this->classConstantReferences; + } + + $result = []; + foreach ($this->phpDocNodes as $phpDocNode) { + $this->collectConstFetchNodes($phpDocNode, $nameScope, $result); + } + + $this->classConstantReferences = $result; + + return $result; + } + + /** + * @param PhpDocClassConstantReference[] $result + */ + private function collectConstFetchNodes(PhpDocAstNode $node, NameScope $nameScope, array &$result): void + { + if ($node instanceof ConstFetchNode) { + if ($node->className !== '') { + if (in_array(strtolower($node->className), ['self', 'static'], true)) { + $className = $nameScope->getClassName(); + } else { + $className = $nameScope->resolveStringName($node->className); + } + + if ($className !== null) { + $result[] = new PhpDocClassConstantReference($className, $node->name); + } + } + return; + } + + foreach (get_object_vars($node) as $prop) { + if ($prop instanceof PhpDocAstNode) { + $this->collectConstFetchNodes($prop, $nameScope, $result); + } elseif (is_array($prop)) { + foreach ($prop as $item) { + if (!($item instanceof PhpDocAstNode)) { + continue; + } + $this->collectConstFetchNodes($item, $nameScope, $result); + } + } + } + } + } diff --git a/src/Rules/DeadCode/UnusedPrivateConstantRule.php b/src/Rules/DeadCode/UnusedPrivateConstantRule.php index 64c50b55a6b..dd64bd17da9 100644 --- a/src/Rules/DeadCode/UnusedPrivateConstantRule.php +++ b/src/Rules/DeadCode/UnusedPrivateConstantRule.php @@ -17,7 +17,6 @@ use function sprintf; use function str_contains; use function str_replace; -use function strtolower; /** * @implements Rule @@ -124,7 +123,7 @@ private function removePhpDocReferencedConstants(ClassConstantsNode $node, Class $className = $classReflection->getName(); foreach ($node->getPhpDocFetches() as $phpDocFetch) { - if (!$this->isSameClass($phpDocFetch->getClassName(), $className)) { + if ($phpDocFetch->getClassName() !== $className) { continue; } @@ -148,10 +147,4 @@ private function removePhpDocReferencedConstants(ClassConstantsNode $node, Class } } - private function isSameClass(string $fetchedClassName, string $className): bool - { - $lower = strtolower($fetchedClassName); - return $lower === 'self' || $lower === 'static' || strtolower($className) === strtolower($fetchedClassName); - } - }