diff --git a/src/Type/Accessory/HasMethodType.php b/src/Type/Accessory/HasMethodType.php index b59a77c788..0cf030e2c8 100644 --- a/src/Type/Accessory/HasMethodType.php +++ b/src/Type/Accessory/HasMethodType.php @@ -14,6 +14,7 @@ use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\StringType; @@ -57,6 +58,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 ba2dbf8892..c164e62592 100644 --- a/src/Type/Accessory/HasPropertyType.php +++ b/src/Type/Accessory/HasPropertyType.php @@ -10,6 +10,7 @@ use PHPStan\Type\AcceptsResult; use PHPStan\Type\CompoundType; use PHPStan\Type\ErrorType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\IntersectionType; use PHPStan\Type\IsSuperTypeOfResult; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -51,6 +52,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 49bbfe8c1b..7e7da791f9 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -295,6 +295,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 ab1091846a..e77e8837f4 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; @@ -217,6 +218,11 @@ public function getEnumCases(): array 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 16cb33d2dd..7999a06efd 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -497,6 +497,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 ad8d4fbfb1..96ca580228 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -386,6 +386,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 5fc9a05280..9f52726739 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -113,6 +113,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 1d5153f731..af0e0399a9 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -52,6 +52,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 361190316b..381e6fd429 100644 --- a/src/Type/ObjectShapeType.php +++ b/src/Type/ObjectShapeType.php @@ -21,6 +21,7 @@ use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericClassStringType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\Generic\TemplateTypeVariance; use PHPStan\Type\Traits\NonGeneralizableTypeTrait; @@ -89,6 +90,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 3b7a72baaa..3de4ce9fe0 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; @@ -916,6 +917,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 9823dcaf80..f0c2ac3cdb 100644 --- a/src/Type/ObjectWithoutClassType.php +++ b/src/Type/ObjectWithoutClassType.php @@ -49,6 +49,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 21622b5b93..952f8c4f89 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -194,6 +194,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 71678b77a4..ce5a482892 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -110,6 +110,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 d35ee72461..18092a0c62 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -88,6 +88,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 0d55341b46..35091c2e39 100644 --- a/src/Type/Traits/NonObjectTypeTrait.php +++ b/src/Type/Traits/NonObjectTypeTrait.php @@ -21,6 +21,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 e9cbca0b7a..eaa9aed951 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -40,6 +40,8 @@ public function getObjectClassNames(): array; */ public function getObjectClassReflections(): array; + public function getClassStringType(): Type; + /** * Returns object type Foo for class-string and 'Foo' (if Foo is a valid class). */ diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 178e72eeb9..9f82e04e7a 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 new file mode 100644 index 0000000000..4480792eb2 --- /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/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 @@ +