From 712635ae9eceab16e841dfb49500240700ebfbec Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 19 Oct 2025 19:41:39 +0200 Subject: [PATCH 1/6] Improve get_class return type --- .../GetClassDynamicReturnTypeExtension.php | 50 +++++++++-------- tests/PHPStan/Analyser/nsrt/bug-4890.php | 53 +++++++++++++++++++ tests/PHPStan/Analyser/nsrt/generics.php | 2 +- ...mpossibleCheckTypeFunctionCallRuleTest.php | 6 +++ .../Rules/Comparison/data/bug-4890b.php | 18 +++++++ 5 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-4890.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-4890b.php diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 14a06d90c0..487504a7d8 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -14,11 +14,12 @@ use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; @@ -66,30 +67,35 @@ static function (Type $type, callable $traverse): Type { return new GenericClassStringType(new ObjectType($type->getClassName())); } - $objectClassNames = $type->getObjectClassNames(); - if ($type instanceof TemplateType && $objectClassNames === []) { - if ($type instanceof ObjectWithoutClassType) { - return new GenericClassStringType($type); - } - - return new UnionType([ - new GenericClassStringType($type), - new ConstantBooleanType(false), - ]); - } elseif ($type instanceof MixedType) { - return new UnionType([ - new ClassStringType(), - new ConstantBooleanType(false), - ]); - } elseif ($type instanceof StaticType) { - return new GenericClassStringType($type->getStaticObjectType()); - } elseif ($objectClassNames !== []) { - return new GenericClassStringType($type); - } elseif ($type instanceof ObjectWithoutClassType) { + if ($type instanceof ObjectShapeType) { return new ClassStringType(); } - return new ConstantBooleanType(false); + $isObject = $type->isObject(); + if ($isObject->no()) { + return new ConstantBooleanType(false); + } + + if ($type instanceof StaticType) { + $objectType = $type->getStaticObjectType(); + } else { + $objectType = TypeCombinator::intersect($type, new ObjectWithoutClassType()); + } + + if (!$objectType instanceof TemplateType && $objectType instanceof ObjectWithoutClassType) { + $classStringType = new ClassStringType(); + } else { + $classStringType = new GenericClassStringType($objectType); + } + + if ($isObject->yes()) { + return $classStringType; + } + + return new UnionType([ + $classStringType, + new ConstantBooleanType(false), + ]); }, ); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890.php b/tests/PHPStan/Analyser/nsrt/bug-4890.php new file mode 100644 index 0000000000..0dbd26de19 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4890.php @@ -0,0 +1,53 @@ +', get_class($entity)); + + if ($entity instanceof Proxy) { + assertType('class-string', get_class($entity)); + } + + $class = $entity instanceof Proxy + ? get_parent_class($entity) + : get_class($entity); + assert(is_string($class)); + + } + + public function updateProp(object $entity): void + { + assertType('class-string', get_class($entity)); + assert(property_exists($entity, 'myProp')); + assertType('class-string', get_class($entity)); + + if ($entity instanceof Proxy) { + assertType('class-string', get_class($entity)); + } + + $class = $entity instanceof Proxy + ? get_parent_class($entity) + : get_class($entity); + assert(is_string($class)); + } + + /** + * @param object{foo: self, bar: int, baz?: string} $entity + */ + public function updateObjectShape($entity): void + { + assertType('class-string', get_class($entity)); + assert(property_exists($entity, 'foo')); + assertType('class-string', get_class($entity)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 77b9a470a6..9093763f45 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -1316,7 +1316,7 @@ function arrayOfGenericClassStrings(array $a): void function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) { assertType( - 'class-string|false', + 'class-string|false', get_class($a) ); assertType( diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index b6f79eeeeb..4a1059ffba 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -927,6 +927,12 @@ public function testLooseComparisonAgainstEnumsNoPhpdoc(): void $this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues); } + public function testBug4890b(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-4890b.php'], []); + } + public function testBug10502(): void { $tipText = 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.'; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-4890b.php b/tests/PHPStan/Rules/Comparison/data/bug-4890b.php new file mode 100644 index 0000000000..8f31e109b3 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-4890b.php @@ -0,0 +1,18 @@ + Date: Sat, 6 Dec 2025 13:41:20 +0100 Subject: [PATCH 2/6] Remove condition --- src/Type/Php/GetClassDynamicReturnTypeExtension.php | 5 ----- tests/PHPStan/Analyser/nsrt/bug-4890.php | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index 487504a7d8..e363214cb0 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -14,7 +14,6 @@ use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectShapeType; use PHPStan\Type\ObjectType; use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\StaticType; @@ -67,10 +66,6 @@ static function (Type $type, callable $traverse): Type { return new GenericClassStringType(new ObjectType($type->getClassName())); } - if ($type instanceof ObjectShapeType) { - return new ClassStringType(); - } - $isObject = $type->isObject(); if ($isObject->no()) { return new ConstantBooleanType(false); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890.php b/tests/PHPStan/Analyser/nsrt/bug-4890.php index 0dbd26de19..4c63fe7712 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4890.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4890.php @@ -46,8 +46,8 @@ public function updateProp(object $entity): void */ public function updateObjectShape($entity): void { - assertType('class-string', get_class($entity)); + assertType('class-string', get_class($entity)); assert(property_exists($entity, 'foo')); - assertType('class-string', get_class($entity)); + assertType('class-string', get_class($entity)); } } From a496efaaf78ca3c52265d13daba41362c797f23b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 6 Dec 2025 14:28:41 +0100 Subject: [PATCH 3/6] Introduce new method --- src/Type/Accessory/HasMethodType.php | 6 +++++ src/Type/Accessory/HasPropertyType.php | 6 +++++ src/Type/ClosureType.php | 5 ++++ src/Type/Enum/EnumCaseObjectType.php | 6 +++++ src/Type/Generic/TemplateMixedType.php | 5 ++++ .../TemplateObjectWithoutClassType.php | 5 ++++ src/Type/IntersectionType.php | 5 ++++ src/Type/MixedType.php | 5 ++++ src/Type/NeverType.php | 5 ++++ src/Type/NonexistentParentClassType.php | 5 ++++ src/Type/ObjectShapeType.php | 6 +++++ src/Type/ObjectType.php | 6 +++++ src/Type/ObjectWithoutClassType.php | 5 ++++ .../GetClassDynamicReturnTypeExtension.php | 24 +------------------ src/Type/StaticType.php | 5 ++++ src/Type/StrictMixedType.php | 5 ++++ src/Type/Traits/LateResolvableTypeTrait.php | 5 ++++ src/Type/Traits/MaybeObjectTypeTrait.php | 6 +++++ src/Type/Traits/NonObjectTypeTrait.php | 5 ++++ src/Type/Type.php | 2 ++ src/Type/UnionType.php | 5 ++++ tests/PHPStan/Analyser/nsrt/bug-4890.php | 4 ++-- tests/PHPStan/Analyser/nsrt/generics.php | 2 +- 23 files changed, 107 insertions(+), 26 deletions(-) diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index 66d8365595..0debee1062 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -15,6 +15,7 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\StringType; @@ -58,6 +59,11 @@ public function getObjectClassReflections(): array return []; } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + private function getCanonicalMethodName(): string { return strtolower($this->methodName); diff --git a/src/Type/Accessory/HasPropertyType.php b/src/Type/Accessory/HasPropertyType.php index a364e32731..de88c702d7 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -11,6 +11,7 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\Enum\EnumCaseObjectType; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -52,6 +53,11 @@ public function getObjectClassReflections(): array return []; } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + public function getConstantStrings(): array { return []; diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index b9267e05c5..2067e5ba68 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -297,6 +297,11 @@ public function isObject(): TrinaryLogic return $this->objectType->isObject(); } + public function getClassStringType(): Type + { + return $this->objectType->getClassStringType(); + } + public function isEnum(): TrinaryLogic { return $this->objectType->isEnum(); diff --git a/src/Type/Enum/EnumCaseObjectType.php b/src/Type/Enum/EnumCaseObjectType.php index e9a32ab039..b77b957f3b 100644 --- a/src/Type/Enum/EnumCaseObjectType.php +++ b/src/Type/Enum/EnumCaseObjectType.php @@ -18,6 +18,7 @@ use PHPStan\Type\CompoundType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\GeneralizePrecision; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\NeverType; use PHPStan\Type\ObjectType; @@ -222,6 +223,11 @@ public function getEnumCaseObject(): ?EnumCaseObjectType return $this; } + public function getClassStringType(): Type + { + return new GenericClassStringType(new ObjectType($this->getClassName())); + } + public function toPhpDocNode(): TypeNode { return new ConstTypeNode( diff --git a/src/Type/Generic/TemplateMixedType.php b/src/Type/Generic/TemplateMixedType.php index 8160633fc3..d7729cc353 100644 --- a/src/Type/Generic/TemplateMixedType.php +++ b/src/Type/Generic/TemplateMixedType.php @@ -63,4 +63,9 @@ public function toStrictMixedType(): TemplateStrictMixedType ); } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + } diff --git a/src/Type/Generic/TemplateObjectWithoutClassType.php b/src/Type/Generic/TemplateObjectWithoutClassType.php index 7d6aebc6f9..68e259132c 100644 --- a/src/Type/Generic/TemplateObjectWithoutClassType.php +++ b/src/Type/Generic/TemplateObjectWithoutClassType.php @@ -36,4 +36,9 @@ public function __construct( $this->default = $default; } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 60090bdfaa..5075f31a37 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -520,6 +520,11 @@ public function isObject(): TrinaryLogic return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject()); } + public function getClassStringType(): Type + { + return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringType()); + } + public function isEnum(): TrinaryLogic { return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 80a0bc2f0b..0c1892e01e 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -392,6 +392,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getClassStringType(): Type + { + return new ClassStringType(); + } + public function isEnum(): TrinaryLogic { if ($this->subtractedType !== null) { diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index c762202f0b..2da6f5e9fa 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -114,6 +114,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getClassStringType(): Type + { + return new NeverType(); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index 74fb77a130..5c217604f6 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -53,6 +53,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getClassStringType(): Type + { + return new ClassStringType(); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/ObjectShapeType.php b/src/Type/ObjectShapeType.php index 57d48a0d0d..9dedcfa148 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -22,6 +22,7 @@ use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -90,6 +91,11 @@ public function getObjectClassReflections(): array return []; } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + public function hasProperty(string $propertyName): TrinaryLogic { return $this->hasInstanceProperty($propertyName); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 85fcc4bf05..b786d1fa72 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -40,6 +40,7 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\Enum\EnumCaseObjectType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateTypeHelper; use PHPStan\Type\Traits\MaybeIterableTypeTrait; @@ -919,6 +920,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createYes(); } + public function getClassStringType(): Type + { + return new GenericClassStringType($this); + } + public function isEnum(): TrinaryLogic { $classReflection = $this->getClassReflection(); diff --git a/src/Type/ObjectWithoutClassType.php b/src/Type/ObjectWithoutClassType.php index c1417f24d6..48f6b1e07b 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -50,6 +50,11 @@ public function getObjectClassReflections(): array return []; } + public function getClassStringType(): Type + { + return new ClassStringType(); + } + public function accepts(Type $type, bool $strictTypes): AcceptsResult { if ($type instanceof CompoundType) { diff --git a/src/Type/Php/GetClassDynamicReturnTypeExtension.php b/src/Type/Php/GetClassDynamicReturnTypeExtension.php index e363214cb0..c79c2126f7 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -10,15 +10,8 @@ use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\Enum\EnumCaseObjectType; -use PHPStan\Type\Generic\GenericClassStringType; -use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\IntersectionType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StaticType; use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; use PHPStan\Type\UnionType; @@ -62,27 +55,12 @@ static function (Type $type, callable $traverse): Type { return $traverse($type); } - if ($type instanceof EnumCaseObjectType) { - return new GenericClassStringType(new ObjectType($type->getClassName())); - } - $isObject = $type->isObject(); if ($isObject->no()) { return new ConstantBooleanType(false); } - if ($type instanceof StaticType) { - $objectType = $type->getStaticObjectType(); - } else { - $objectType = TypeCombinator::intersect($type, new ObjectWithoutClassType()); - } - - if (!$objectType instanceof TemplateType && $objectType instanceof ObjectWithoutClassType) { - $classStringType = new ClassStringType(); - } else { - $classStringType = new GenericClassStringType($objectType); - } - + $classStringType = $type->getClassStringType(); if ($isObject->yes()) { return $classStringType; } diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index 2876bfb15e..3c696eb603 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -198,6 +198,11 @@ public function isObject(): TrinaryLogic return $this->getStaticObjectType()->isObject(); } + public function getClassStringType(): Type + { + return $this->getStaticObjectType()->getClassStringType(); + } + public function isEnum(): TrinaryLogic { return $this->getStaticObjectType()->isEnum(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 953cb19054..af20367941 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -111,6 +111,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getClassStringType(): Type + { + return new ErrorType(); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index 4a5dd1e0b9..4b0dacddd7 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -89,6 +89,11 @@ public function isObject(): TrinaryLogic return $this->resolve()->isObject(); } + public function getClassStringType(): Type + { + return $this->resolve()->getClassStringType(); + } + public function isEnum(): TrinaryLogic { return $this->resolve()->isEnum(); diff --git a/src/Type/Traits/MaybeObjectTypeTrait.php b/src/Type/Traits/MaybeObjectTypeTrait.php index 71e4df3421..d6ff391374 100644 --- a/src/Type/Traits/MaybeObjectTypeTrait.php +++ b/src/Type/Traits/MaybeObjectTypeTrait.php @@ -14,6 +14,7 @@ use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection; use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\ClassStringType; use PHPStan\Type\MixedType; use PHPStan\Type\Type; @@ -30,6 +31,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createMaybe(); } + public function getClassStringType(): Type + { + return new ClassStringType(); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createMaybe(); diff --git a/src/Type/Traits/NonObjectTypeTrait.php b/src/Type/Traits/NonObjectTypeTrait.php index c441d3204c..21eb0e8aed 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -22,6 +22,11 @@ public function isObject(): TrinaryLogic return TrinaryLogic::createNo(); } + public function getClassStringType(): Type + { + return new ErrorType(); + } + public function isEnum(): TrinaryLogic { return TrinaryLogic::createNo(); diff --git a/src/Type/Type.php b/src/Type/Type.php index b5de81b99c..d537055f50 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -68,6 +68,8 @@ public function getObjectClassNames(): array; /** @return list */ public function getObjectClassReflections(): array; + public function getClassStringType(): Type; + /** * Returns the object type for a class-string or literal class name string. * For non-class-string types, returns ErrorType. diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index c5c9f65415..aa3d1b9b04 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -449,6 +449,11 @@ public function isObject(): TrinaryLogic return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject()); } + public function getClassStringType(): Type + { + return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringType()); + } + public function isEnum(): TrinaryLogic { return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum()); diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890.php b/tests/PHPStan/Analyser/nsrt/bug-4890.php index 4c63fe7712..4480792eb2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4890.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4890.php @@ -12,7 +12,7 @@ public function update(object $entity): void { assertType('class-string', get_class($entity)); assert(method_exists($entity, 'getId')); - assertType('class-string', get_class($entity)); + assertType('class-string', get_class($entity)); if ($entity instanceof Proxy) { assertType('class-string', get_class($entity)); @@ -29,7 +29,7 @@ public function updateProp(object $entity): void { assertType('class-string', get_class($entity)); assert(property_exists($entity, 'myProp')); - assertType('class-string', get_class($entity)); + assertType('class-string', get_class($entity)); if ($entity instanceof Proxy) { assertType('class-string', get_class($entity)); diff --git a/tests/PHPStan/Analyser/nsrt/generics.php b/tests/PHPStan/Analyser/nsrt/generics.php index 9093763f45..77b9a470a6 100644 --- a/tests/PHPStan/Analyser/nsrt/generics.php +++ b/tests/PHPStan/Analyser/nsrt/generics.php @@ -1316,7 +1316,7 @@ function arrayOfGenericClassStrings(array $a): void function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject) { assertType( - 'class-string|false', + 'class-string|false', get_class($a) ); assertType( From 0fb0665cb984f26e28bf36fa48c300e932795ceb Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 10 Feb 2026 22:14:01 +0100 Subject: [PATCH 4/6] Add comment --- src/Type/Type.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Type/Type.php b/src/Type/Type.php index d537055f50..9af6fcf203 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -68,6 +68,9 @@ public function getObjectClassNames(): array; /** @return list */ public function getObjectClassReflections(): array; + /** + * Return class-string for object type Foo. + */ public function getClassStringType(): Type; /** From bec0a6f0dd0fc24ea6f716def43b8bed43b3abd2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 11 Feb 2026 12:03:04 +0100 Subject: [PATCH 5/6] Add test --- tests/PHPStan/Analyser/nsrt/bug-4890.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890.php b/tests/PHPStan/Analyser/nsrt/bug-4890.php index 4480792eb2..e074d5aa53 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4890.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4890.php @@ -50,4 +50,20 @@ public function updateObjectShape($entity): void assert(property_exists($entity, 'foo')); assertType('class-string', get_class($entity)); } + + /** + * @phpstan-template T of Proxy + * + * @param T $entity + * + * @return T + */ + public function updateGeneric($entity): object + { + assertType('class-string', get_class($entity)); + assert(property_exists($entity, 'myProp')); + assertType('class-string', get_class($entity)); + + return $entity; + } } From f409b6cf92586429a6ccee068cc33967ad44bfb5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 11 Feb 2026 18:00:18 +0100 Subject: [PATCH 6/6] Add test --- tests/PHPStan/Analyser/nsrt/bug-4890-php8.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-4890-php8.php diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890-php8.php b/tests/PHPStan/Analyser/nsrt/bug-4890-php8.php new file mode 100644 index 0000000000..609fb2661c --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4890-php8.php @@ -0,0 +1,23 @@ += 8.0 + +namespace Bug4890Php8; + +use function PHPStan\Testing\assertType; + +enum MyEnum +{ + case CASE1; + case CASE2; + + public function someMethod(): bool { return true; } +} + +class HelloWorld +{ + public function withEnumCase(\UnitEnum $entity): void + { + assertType('class-string', get_class($entity)); + assert(method_exists($entity, 'someMethod')); + assertType('class-string', get_class($entity)); + } +}