From 1535226b867485cb17d69d49452ae2e1fe4afc4c Mon Sep 17 00:00:00 2001 From: phpstan-bot <79867460+phpstan-bot@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:51:51 +0000 Subject: [PATCH 1/4] Consider class and constant finality in `ClassConstantAccessType::getResult()` for `static::CONST` PHPDoc types - Fix `ClassConstantAccessType::getResult()` to check whether the class is final or the constant is final before returning the concrete constant value. For non-final classes with non-final, untyped constants, return `MixedType` instead of the concrete value, matching the behavior of `InitializerExprTypeResolver` for `static::CONST` expressions in code. - Override `isSubTypeOf()` and `isAcceptedBy()` to use `getValueType()` directly, ensuring `ClassConstantAccessType` survives `TypehintHelper::decideType()` and can be resolved correctly after `StaticType` is substituted with the caller's concrete type. - Update test assertions in `bug-13828.php` and `bug-6989.php` that incorrectly expected concrete constant values for non-final classes. - Add regression test `bug-14556.php` for the reported issue. --- src/Type/ClassConstantAccessType.php | 46 +++++++++++++++++++++-- tests/PHPStan/Analyser/nsrt/bug-13828.php | 6 +-- tests/PHPStan/Analyser/nsrt/bug-14556.php | 33 ++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-6989.php | 4 +- 4 files changed, 81 insertions(+), 8 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14556.php diff --git a/src/Type/ClassConstantAccessType.php b/src/Type/ClassConstantAccessType.php index e3be822f447..6b44b48106a 100644 --- a/src/Type/ClassConstantAccessType.php +++ b/src/Type/ClassConstantAccessType.php @@ -8,6 +8,7 @@ use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\LateResolvableTypeTrait; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; +use function count; final class ClassConstantAccessType implements CompoundType, LateResolvableType { @@ -49,13 +50,52 @@ public function isResolvable(): bool return !TypeUtils::containsTemplateType($this->type); } - protected function getResult(): Type + public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult { if ($this->type->hasConstant($this->constantName)->yes()) { - return $this->type->getConstant($this->constantName)->getValueType(); + $valueType = $this->type->getConstant($this->constantName)->getValueType(); + return $otherType->isSuperTypeOf($valueType); + } + + return $otherType->isSuperTypeOf($this->resolve()); + } + + public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult + { + if ($this->type->hasConstant($this->constantName)->yes()) { + $valueType = $this->type->getConstant($this->constantName)->getValueType(); + return $acceptingType->accepts($valueType, $strictTypes); + } + + $result = $this->resolve(); + + if ($result instanceof CompoundType) { + return $result->isAcceptedBy($acceptingType, $strictTypes); + } + + return $acceptingType->accepts($result, $strictTypes); + } + + protected function getResult(): Type + { + if (!$this->type->hasConstant($this->constantName)->yes()) { + return new ErrorType(); + } + + $constantReflection = $this->type->getConstant($this->constantName); + + $classReflections = $this->type->getObjectClassReflections(); + $isFinalClass = count($classReflections) === 1 && $classReflections[0]->isFinal(); + + if ($isFinalClass || $constantReflection->isFinal()) { + return $constantReflection->getValueType(); + } + + if (!$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType()) { + return new MixedType(); } - return new ErrorType(); + return $constantReflection->getValueType(); } /** diff --git a/tests/PHPStan/Analyser/nsrt/bug-13828.php b/tests/PHPStan/Analyser/nsrt/bug-13828.php index 551b694536b..d854531cfcb 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13828.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13828.php @@ -22,8 +22,8 @@ class BarBaz extends FooBar function test(FooBar $foo, BarBaz $bar): void { - assertType("'foo'", $foo->test()); - assertType("'bar'", $bar->test()); + assertType('mixed', $foo->test()); + assertType('mixed', $bar->test()); } final class FinalFoo @@ -146,7 +146,7 @@ public function test(): string function testUntypedConstant(WithUntypedConstant $foo): void { - assertType("'foo'", $foo->test()); + assertType('mixed', $foo->test()); } final class FinalChild extends FooBar diff --git a/tests/PHPStan/Analyser/nsrt/bug-14556.php b/tests/PHPStan/Analyser/nsrt/bug-14556.php new file mode 100644 index 00000000000..79af53005d9 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14556.php @@ -0,0 +1,33 @@ +test()); + assertType('mixed', $bar->test()); + assertType("'bar'", $baz->test()); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-6989.php b/tests/PHPStan/Analyser/nsrt/bug-6989.php index 3ea5dbe4b33..9ce61ee636d 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-6989.php +++ b/tests/PHPStan/Analyser/nsrt/bug-6989.php @@ -17,7 +17,7 @@ class MyClass */ public function myMethod(array $items1, array $items2, array $items3): array { - assertType('array{key: string}', $items1); + assertType('non-empty-array', $items1); assertType('array{key: string}', $items2); assertType('array{key: string}', $items3); @@ -40,7 +40,7 @@ class ParentClass extends MyClass */ public function myMethod2(array $items1, array $items2, array $items3, array $items4, array $items5): array { - assertType('array{different_key: string}', $items1); + assertType('non-empty-array', $items1); assertType('array{different_key: string}', $items2); assertType('array{key: string}', $items3); assertType('array{different_key: string}', $items4); From 962a3cfa739139b21f4f39bec0816b220a7e1e36 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 29 Apr 2026 17:14:13 +0000 Subject: [PATCH 2/4] Fix toPhpDocNode() to use actual type instead of hardcoded 'static' Co-Authored-By: Claude Opus 4.6 --- src/Type/ClassConstantAccessType.php | 2 +- tests/PHPStan/Type/TypeToPhpDocNodeTest.php | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Type/ClassConstantAccessType.php b/src/Type/ClassConstantAccessType.php index 6b44b48106a..6d5d97c0504 100644 --- a/src/Type/ClassConstantAccessType.php +++ b/src/Type/ClassConstantAccessType.php @@ -129,7 +129,7 @@ public function traverseSimultaneously(Type $right, callable $cb): Type public function toPhpDocNode(): TypeNode { - return new ConstTypeNode(new ConstFetchNode('static', $this->constantName)); + return new ConstTypeNode(new ConstFetchNode((string) $this->type->toPhpDocNode(), $this->constantName)); } } diff --git a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php index 3cf9e5f3fa6..df83d849578 100644 --- a/tests/PHPStan/Type/TypeToPhpDocNodeTest.php +++ b/tests/PHPStan/Type/TypeToPhpDocNodeTest.php @@ -522,6 +522,18 @@ public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable new ConstantFloatType(-0.0), '-0.0', ]; + + $reflectionProvider = self::createReflectionProvider(); + + yield [ + new ClassConstantAccessType(new StaticType($reflectionProvider->getClass(stdClass::class)), 'FOO'), + 'static::FOO', + ]; + + yield [ + new ClassConstantAccessType(new ObjectType('stdClass'), 'FOO'), + 'stdClass::FOO', + ]; } #[DataProvider('dataToPhpDocNodeWithoutCheckingEquals')] From a9108945bf87cf09c67dac486addf0e293ff3ea6 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 15:00:19 +0000 Subject: [PATCH 3/4] Add comprehensive tests for static::CONST with PHPDoc types, native types, final constants and final classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probes which type is used for each combination: - Final class + native type → concrete value - Final class + PHPDoc type → concrete value - Final class + both types → concrete value - Non-final class + final const + PHPDoc type → declared PHPDoc type - Non-final class + final const + native type → declared native type Co-Authored-By: Claude Opus 4.6 --- tests/PHPStan/Analyser/nsrt/bug-13828.php | 83 +++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-13828.php b/tests/PHPStan/Analyser/nsrt/bug-13828.php index d854531cfcb..69f52200ea4 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13828.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13828.php @@ -175,3 +175,86 @@ function testFinalTypedConstant(WithFinalTypedConstant $foo): void { assertType('non-empty-string', $foo->test()); } + +final class FinalClassWithNativeType +{ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithNativeType(FinalClassWithNativeType $foo): void +{ + assertType("'foo'", $foo->test()); +} + +final class FinalClassWithPhpDocType +{ + /** @var non-empty-string */ + const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithPhpDocType(FinalClassWithPhpDocType $foo): void +{ + assertType("'foo'", $foo->test()); +} + +final class FinalClassWithBothTypes +{ + /** @var non-empty-string */ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalClassWithBothTypes(FinalClassWithBothTypes $foo): void +{ + assertType("'foo'", $foo->test()); +} + +class WithFinalPhpDocConstant +{ + /** @var non-empty-string */ + final const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalPhpDocConstant(WithFinalPhpDocConstant $foo): void +{ + assertType('non-empty-string', $foo->test()); +} + +class WithFinalNativeConstant +{ + final const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalNativeConstant(WithFinalNativeConstant $foo): void +{ + assertType('string', $foo->test()); +} From 0292be81be497d51f1788f3e9d7f8bf2a3c2872e Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Wed, 6 May 2026 15:46:31 +0000 Subject: [PATCH 4/4] Return exact value for final constants in ClassConstantAccessType::getResult() Add getInitializerExprType() to ClassConstantReflection to retrieve the concrete initializer value type, bypassing PHPDoc/native type declarations. Use it in ClassConstantAccessType::getResult() for final constants, since they cannot be overridden and their exact value is always known. Update test expectations for final typed constants to expect exact values instead of declared types. Add test cases for non-final class with non-final typed constants to verify they correctly resolve to PHPDoc/native types. Co-Authored-By: Claude Opus 4.6 --- src/Reflection/ClassConstantReflection.php | 2 + .../Dummy/DummyClassConstantReflection.php | 5 ++ .../RealClassClassConstantReflection.php | 5 ++ ...nDeclaringClassClassConstantReflection.php | 5 ++ src/Type/ClassConstantAccessType.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-13828.php | 6 +- tests/PHPStan/Analyser/nsrt/bug-14556.php | 66 +++++++++++++++++++ 7 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/Reflection/ClassConstantReflection.php b/src/Reflection/ClassConstantReflection.php index 097082b7618..afc4b32df56 100644 --- a/src/Reflection/ClassConstantReflection.php +++ b/src/Reflection/ClassConstantReflection.php @@ -40,4 +40,6 @@ public function getNativeType(): ?Type; public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock; + public function getInitializerExprType(): Type; + } diff --git a/src/Reflection/Dummy/DummyClassConstantReflection.php b/src/Reflection/Dummy/DummyClassConstantReflection.php index 768c5bdf275..13de0a5551f 100644 --- a/src/Reflection/Dummy/DummyClassConstantReflection.php +++ b/src/Reflection/Dummy/DummyClassConstantReflection.php @@ -122,4 +122,9 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return null; } + public function getInitializerExprType(): Type + { + return new MixedType(); + } + } diff --git a/src/Reflection/RealClassClassConstantReflection.php b/src/Reflection/RealClassClassConstantReflection.php index d0b69f5eedc..01b6b0d2ae0 100644 --- a/src/Reflection/RealClassClassConstantReflection.php +++ b/src/Reflection/RealClassClassConstantReflection.php @@ -91,6 +91,11 @@ public function getValueType(): Type return $this->valueType; } + public function getInitializerExprType(): Type + { + return $this->initializerExprTypeResolver->getType($this->getValueExpr(), InitializerExprContext::fromClassReflection($this->declaringClass)); + } + public function getDeclaringClass(): ClassReflection { return $this->declaringClass; diff --git a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php index a92a6172737..60da209c3af 100644 --- a/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php +++ b/src/Rules/RestrictedUsage/RewrittenDeclaringClassClassConstantReflection.php @@ -119,4 +119,9 @@ public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock return $this->constantReflection->getResolvedPhpDoc(); } + public function getInitializerExprType(): Type + { + return $this->constantReflection->getInitializerExprType(); + } + } diff --git a/src/Type/ClassConstantAccessType.php b/src/Type/ClassConstantAccessType.php index 6d5d97c0504..34f1b493128 100644 --- a/src/Type/ClassConstantAccessType.php +++ b/src/Type/ClassConstantAccessType.php @@ -88,7 +88,7 @@ protected function getResult(): Type $isFinalClass = count($classReflections) === 1 && $classReflections[0]->isFinal(); if ($isFinalClass || $constantReflection->isFinal()) { - return $constantReflection->getValueType(); + return $constantReflection->getInitializerExprType(); } if (!$constantReflection->hasPhpDocType() && !$constantReflection->hasNativeType()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13828.php b/tests/PHPStan/Analyser/nsrt/bug-13828.php index 69f52200ea4..71dd98fc982 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-13828.php +++ b/tests/PHPStan/Analyser/nsrt/bug-13828.php @@ -173,7 +173,7 @@ public function test(): string function testFinalTypedConstant(WithFinalTypedConstant $foo): void { - assertType('non-empty-string', $foo->test()); + assertType("'foo'", $foo->test()); } final class FinalClassWithNativeType @@ -240,7 +240,7 @@ public function test(): string function testFinalPhpDocConstant(WithFinalPhpDocConstant $foo): void { - assertType('non-empty-string', $foo->test()); + assertType("'foo'", $foo->test()); } class WithFinalNativeConstant @@ -256,5 +256,5 @@ public function test(): string function testFinalNativeConstant(WithFinalNativeConstant $foo): void { - assertType('string', $foo->test()); + assertType("'foo'", $foo->test()); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14556.php b/tests/PHPStan/Analyser/nsrt/bug-14556.php index 79af53005d9..472b7bb447a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14556.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14556.php @@ -31,3 +31,69 @@ function test(FooBar $foo, BarBaz $bar, FinalBarBaz $baz): void assertType('mixed', $bar->test()); assertType("'bar'", $baz->test()); } + +class WithNativeType +{ + const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testNativeType(WithNativeType $foo): void +{ + assertType('string', $foo->test()); +} + +class WithPhpDocType +{ + /** @var non-empty-string */ + const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testPhpDocType(WithPhpDocType $foo): void +{ + assertType('non-empty-string', $foo->test()); +} + +class WithFinalConstant +{ + final const FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalConstant(WithFinalConstant $foo): void +{ + assertType("'foo'", $foo->test()); +} + +class WithFinalTypedConstant +{ + /** @var non-empty-string */ + final const string FOO_BAR = 'foo'; + + /** @return static::FOO_BAR */ + public function test(): string + { + return static::FOO_BAR; + } +} + +function testFinalTypedConstant(WithFinalTypedConstant $foo): void +{ + assertType("'foo'", $foo->test()); +}