Fix #13828: Reference to static const behaves as self const#4906
Fix #13828: Reference to static const behaves as self const#4906phpstan-bot wants to merge 7 commits into2.1.xfrom
Conversation
ondrejmirtes
left a comment
There was a problem hiding this comment.
This is not entirelt correct. When the constant has a native type or a PHPDoc type, use that when accessed on an expression if the class is not final. Generate extra tests for this scenario and then fix them.
ondrejmirtes
left a comment
There was a problem hiding this comment.
@phpstan-bot This is not entirelt correct. When the constant has a native type or a PHPDoc type, use that when accessed on an expression if the class is not final. Generate extra tests for this scenario and then fix them.
|
I'll analyze this and get back to you. |
ondrejmirtes
left a comment
There was a problem hiding this comment.
@phpstan-bot This is not entirelt correct. When the constant has a native type or a PHPDoc type, use that when accessed on an expression if the class is not final. Generate extra tests for this scenario and then fix them.
|
Claude encountered an error —— View job Working on the requested changes… Tasks
Changes made
|
|
@phpstan-bot You crashed last time. Now I'm retrying without --max-turns option. The diff is not entirely correct. When the constant has a native type or a PHPDoc type, use that when accessed on an expression if the class is not final. Generate extra tests for this scenario and then fix them. |
a2c60cd to
b0abcf3
Compare
|
Claude finished @ondrejmirtes's task in 9m 26s —— View job Tasks
Changes made
The key insight: when a method with
|
| function test(FooBar $foo, BarBaz $bar): void | ||
| { | ||
| assertType("'foo'", $foo->test()); | ||
| assertType("'bar'", $bar->test()); |
There was a problem hiding this comment.
@phpstan-bot These are still wrong. the constant does not have PHPDoc type, nor native type. The classes are not final. A subclass might change the constant value to something completely else. The right type to use here is mixed.
ondrejmirtes
left a comment
There was a problem hiding this comment.
@phpstan-bot react to last review pls
|
Claude finished @ondrejmirtes's task in 3m 12s —— View job Tasks
Changes
Logic:
|
ondrejmirtes
left a comment
There was a problem hiding this comment.
@phpstan-bot We must have entirely the same logic for ClassConstFetch in MutatingScope resolveType, or maybe initializerExprTypeResolver, it deals with the same problems. Please regiew your code and try to change it to match the current type inference logic
|
Claude finished @ondrejmirtes's task in 8m 36s —— View job Tasks
ChangesRestructured
The condition structure now exactly mirrors the existing code instead of having the inverted nesting from before.
Note: The |
src/Type/ClassConstantAccessType.php
Outdated
| && $this->type->equals($type->type) | ||
| && $this->constantName === $type->constantName; |
There was a problem hiding this comment.
should do the cheap check first
src/Type/ClassConstantAccessType.php
Outdated
| if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($this->constantName)) { | ||
| return new Enum\EnumCaseObjectType($constantClassReflection->getName(), $this->constantName); | ||
| } |
There was a problem hiding this comment.
maybe this could use $this->type()->getEnumCaseObject()
|
@phpstan-bot Check Markus's comments |
|
Claude finished @ondrejmirtes's task in 3m 49s —— View job Addressing Markus's review comments
Changes
Enum case handling (line 70): I investigated staabm's suggestion to use The current pattern ( |
|
@phpstan-bot Please read the failed CI job logs and fix the CI. |
|
Claude finished @ondrejmirtes's task in 11m 34s —— View job Tasks
Root causeThree CI failures, all stemming from two issues: 1. Coding Standard — 2. Test failures (bug-13828, bug-6989) and integration tests — Two fundamental problems:
Fixes
|
- 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
When the inner type is still a StaticType (not yet resolved to a concrete class), use the declared type (native or PHPDoc) instead of the literal value for non-final constants on non-final classes. For final constants or final classes, the literal value is preserved since subclasses cannot override them. Added test cases for: - Constants with native type (const string) - Constants with PHPDoc type (@var non-empty-string) - Constants with both native and PHPDoc types - Final constants on non-final classes - Untyped constants Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
…non-final classes When static::CONST is used and the constant has no native type or PHPDoc type, and neither the class nor the constant is final, return mixed instead of the literal value — since a subclass could override the constant with any value. Also unified the logic to use getObjectClassReflections() instead of instanceof StaticType, so it works for both unresolved StaticType and resolved ObjectType (e.g., FooBar $foo where $foo could be a subclass). Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
…solver logic Restructured getResult() to match the $isObject path in InitializerExprTypeResolver::getClassConstFetchTypeByReflection(): - Extract ClassReflection first via getObjectClassReflections() - Add enum case handling (EnumCaseObjectType) - Match exact condition structure: class not final AND constant not final AND no phpDocType AND no nativeType → mixed - Otherwise use getValueType() (declared type for typed constants, literal for untyped constants) Added test cases for final child class inheriting @return static::CONST and for final typed constant on non-final class. Co-authored-by: Ondřej Mirtes <ondrejmirtes@users.noreply.github.com>
…comparison Co-authored-by: Markus Staab <markus.staab@redaxo.de> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
86aa740 to
486393a
Compare
Automated fix attempt 1 for CI failures.
Automated fix attempt 2 for CI failures.

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