Fix #13828: Reference to static const behaves as self const #4906
+179
−0
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Summary
When a parent class declared
@return static::SOME_CONSTin a method's PHPDoc, PHPStan treatedstatic::identically toself::— 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
src/Type/ClassConstantAccessType.php— aLateResolvableTypethat wraps aType(initiallyStaticType) and a constant name, deferring resolution until the caller type is knownsrc/PhpDoc/TypeNodeResolver.php:resolveConstTypeNode(): separatedstaticandselfcases; forstatic::CONSTon non-final classes, returnsClassConstantAccessType(StaticType, constantName)instead of eagerly resolvingresolveArrayShapeOffsetType(): same fix for array shape key contextsstatic::CONST_*patterns usegetValueType()fallbackCLAUDE.mdwith documentation of the new patternRoot cause
TypeNodeResolver::resolveConstTypeNode()handledstaticandselfin a singlecaseblock, 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 ofstatic::.The fix introduces
ClassConstantAccessType, which preserves theStaticTypeinside the return type. When a method is called on a specific class (e.g.,BarBaz),CalledOnTypeUnresolvedMethodPrototypeReflection::transformStaticType()traverses the return type and replacesStaticTypewithObjectType('BarBaz'). TheClassConstantAccessType::traverse()method creates a new instance with the replaced inner type, andgetResult()then resolves the constant on the correct class — givingBarBaz::FOO_BARinstead ofFooBar::FOO_BAR.Test
Added
tests/PHPStan/Analyser/nsrt/bug-13828.phpwhich verifies:FooBarwithconst FOO_BAR = 'foo'and@return static::FOO_BARresolves to'foo'when called onFooBarBarBaz extends FooBarwithconst FOO_BAR = 'bar'resolves to'bar'when called onBarBaz@return static::FOO_BARresolves to the literal value'foo'Fixes phpstan/phpstan#13828