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 14a06d90c0..c79c2126f7 100644 --- a/src/Type/Php/GetClassDynamicReturnTypeExtension.php +++ b/src/Type/Php/GetClassDynamicReturnTypeExtension.php @@ -10,14 +10,7 @@ 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\MixedType; -use PHPStan\Type\ObjectType; -use PHPStan\Type\ObjectWithoutClassType; -use PHPStan\Type\StaticType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; use PHPStan\Type\TypeUtils; @@ -62,34 +55,20 @@ 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); } - $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) { - return new ClassStringType(); + $classStringType = $type->getClassStringType(); + if ($isObject->yes()) { + return $classStringType; } - return new ConstantBooleanType(false); + return new UnionType([ + $classStringType, + new ConstantBooleanType(false), + ]); }, ); } 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..9af6fcf203 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -68,6 +68,11 @@ public function getObjectClassNames(): array; /** @return list */ public function getObjectClassReflections(): array; + /** + * Return class-string for object type Foo. + */ + 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-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)); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4890.php b/tests/PHPStan/Analyser/nsrt/bug-4890.php new file mode 100644 index 0000000000..e074d5aa53 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-4890.php @@ -0,0 +1,69 @@ +', 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)); + } + + /** + * @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; + } +} 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 @@ +