Skip to content

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When a parent class declared @return static::SOME_CONST in a method's PHPDoc, PHPStan treated static:: identically to self:: — always resolving to the declaring class's constant value. This meant subclass constant overrides were ignored: calling the method on a child class that overrides the constant still returned the parent's value.

Changes

  • Created new src/Type/ClassConstantAccessType.php — a LateResolvableType that wraps a Type (initially StaticType) and a constant name, deferring resolution until the caller type is known
  • Modified src/PhpDoc/TypeNodeResolver.php:
    • resolveConstTypeNode(): separated static and self cases; for static::CONST on non-final classes, returns ClassConstantAccessType(StaticType, constantName) instead of eagerly resolving
    • resolveArrayShapeOffsetType(): same fix for array shape key contexts
    • Wildcard static::CONST_* patterns use getValueType() fallback
  • Updated CLAUDE.md with documentation of the new pattern

Root cause

TypeNodeResolver::resolveConstTypeNode() handled static and self in a single case block, resolving both to $nameScope->getClassName() and then immediately computing the constant's literal value from the declaring class. This lost the late static binding semantics of static::.

The fix introduces ClassConstantAccessType, which preserves the StaticType inside the return type. When a method is called on a specific class (e.g., BarBaz), CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType() traverses the return type and replaces StaticType with ObjectType('BarBaz'). The ClassConstantAccessType::traverse() method creates a new instance with the replaced inner type, and getResult() then resolves the constant on the correct class — giving BarBaz::FOO_BAR instead of FooBar::FOO_BAR.

Test

Added tests/PHPStan/Analyser/nsrt/bug-13828.php which verifies:

  • FooBar with const FOO_BAR = 'foo' and @return static::FOO_BAR resolves to 'foo' when called on FooBar
  • BarBaz extends FooBar with const FOO_BAR = 'bar' resolves to 'bar' when called on BarBaz
  • Final class with @return static::FOO_BAR resolves to the literal value 'foo'

Fixes phpstan/phpstan#13828

- Added ClassConstantAccessType that wraps StaticType + constant name and
  implements LateResolvableType, deferring resolution until the caller type is known
- Modified TypeNodeResolver::resolveConstTypeNode() and resolveArrayShapeOffsetType()
  to create ClassConstantAccessType when the keyword is 'static' instead of
  resolving eagerly like 'self'
- The StaticType inside ClassConstantAccessType gets replaced with the concrete
  ObjectType during CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType(),
  then the constant is resolved on the correct class
- New regression test in tests/PHPStan/Analyser/nsrt/bug-13828.php
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.

Reference to static const behaves as self const

2 participants