Skip to content

Scan PHPDoc type nodes for class constant references in UnusedPrivateConstantRule#5677

Closed
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-5ezeidn
Closed

Scan PHPDoc type nodes for class constant references in UnusedPrivateConstantRule#5677
phpstan-bot wants to merge 3 commits into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-5ezeidn

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

UnusedPrivateConstantRule only checked for constant usages via AST ClassConstFetch nodes (explicit PHP code references like self::CONST). PHPDoc type references such as value-of<self::CONST>, key-of<self::CONST>, int-mask-of<self::FLAG_*>, and const type notation (self::CONST*) did not count as "usage," causing false-positive "unused constant" errors.

Changes

  • Modified src/Rules/DeadCode/UnusedPrivateConstantRule.php:
    • Added removePhpDocReferencedConstants() method that collects resolved PHPDoc blocks from the class-level, all methods, and all properties declared in the class
    • Added findConstFetchReferences() method that recursively walks PHPDoc AST nodes (using get_object_vars) to find ConstFetchNode instances
    • Added isSameClass() helper to match self, static, or the class name
    • Handles wildcard patterns (e.g., FLAG_*) using regex matching, consistent with how TypeNodeResolver handles them
  • Updated tests/PHPStan/Analyser/AnalyserIntegrationTest.php:
    • testBug9039 now expects no errors since self::* in @template-extends correctly references all matching constants including private ones
  • Added tests/PHPStan/Rules/DeadCode/data/bug-6415.php with test cases covering:
    • value-of<self::CONST> in return type
    • key-of<self::CONST> in return type
    • key-of<self::CONST> in property @var
    • int-mask-of<self::FLAG_*> with wildcard
    • self::MODE_* const type notation in @param
    • @phpstan-type alias with value-of<self::CONST>
    • @param value-of<self::CONST> in parameters
    • @phpstan-assert value-of<self::CONST> in assertions
    • Mixed case: PHPDoc-used constant alongside a truly unused constant (verifies unused is still reported)

Root cause

The UnusedPrivateConstantRule relied exclusively on ClassStatementsGatherer which only collects Expr\ClassConstFetch AST nodes from PHP code. PHPDoc types are resolved during type resolution in TypeNodeResolver::resolveConstTypeNode(), which eagerly resolves the constant reference to its value type — the constant reference is consumed and never surfaces as an AST node. The fix adds a second pass that scans the raw PHPDoc AST (before type resolution) for ConstFetchNode instances.

Test

Regression test testBug6415 covers 8 classes exercising all PHPDoc-based constant reference patterns: value-of, key-of, int-mask-of, const type wildcard notation, property types, parameter types, assertion types, and @phpstan-type aliases. The MixedUsage class verifies that truly unused constants are still reported alongside PHPDoc-referenced ones.

Fixes phpstan/phpstan#6415

…eConstantRule`

- 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
Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than checking this in UnusedPrivateConstantRule it would make more sens to add the constantFetch in ClassStatementsGatherer

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 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. I've addressed the review by moving the PHPDoc constant reference scanning logic out of UnusedPrivateConstantRule and into ClassConstantsNode, which is the architectural layer between the gatherer and rules.

Key design decisions:

  • Lazy evaluation in ClassConstantsNode::getPhpDocFetches() — Computing eagerly in ClassStatementsGatherer during traversal caused interference with template type resolution (broke bug-9981 test). The node computes on-demand when the rule requests it.
  • New PhpDocClassConstantReference value object — Clean representation of a PHPDoc constant reference (className + constantName) in src/Node/Constant/.
  • Simplified ruleUnusedPrivateConstantRule now just iterates $node->getPhpDocFetches() and applies wildcard matching, with no direct PHPDoc/reflection access.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The correct way to access PHPDocs is through FileTypeMapper. Also you shouldn't have to do any regexes, but correctly resolve the class name with NameScope.

Of course the issue is that the thing returned by FileTypeMapper is ResolvedPhpDocBlock. Which is about accessing the useful PHPDoc things like ParamTag and many others.

One way to solve this would be to add getClassConstFetches on ResolvedPhpDocBlock. But the class is a bit tricky to work with - because it can be created multiple ways (create, createEmpty, merge), we need to be careful every time a new property is added.

…g 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 <noreply@anthropic.com>
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

The changes are committed and pushed. Here's a summary of what was done to address the review:

Changes made:

  1. src/PhpDoc/ResolvedPhpDocBlock.php — Added getClassConstantReferences() method that:

    • Uses NameScope::resolveStringName() to properly resolve class names (handling self, static, use aliases, and namespaces) — addressing the reviewer's feedback about using NameScope instead of manual regex/string matching
    • Walks PHPDoc AST nodes recursively to find ConstFetchNode instances
    • Returns PhpDocClassConstantReference[] with fully-qualified resolved class names
    • Caches results lazily (with false sentinel pattern matching existing properties)
    • Properly handled in createEmpty() (returns []) and merge() (eagerly computes from child block which has a valid NameScope)
  2. src/Node/ClassConstantsNode.php — Simplified to delegate PHPDoc traversal to ResolvedPhpDocBlock::getClassConstantReferences() instead of doing manual get_object_vars traversal itself. Removed the private collectPhpDocConstantFetches() method and related imports.

  3. src/Rules/DeadCode/UnusedPrivateConstantRule.php — Removed isSameClass() method. Since class names are now resolved via NameScope in ResolvedPhpDocBlock, the rule just compares $phpDocFetch->getClassName() !== $className directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants