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 1968fab471..23f3a1e6c0 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 @@ +