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/2] 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/2] 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')]