Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 28 additions & 22 deletions src/Type/Php/GetClassDynamicReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
]);
},
);
}
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-4890.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Bug4890;

use function PHPStan\Testing\assertType;

interface Proxy {}

class HelloWorld
{
public function update(object $entity): void
{
assertType('class-string', get_class($entity));
assert(method_exists($entity, 'getId'));
assertType('class-string<object&hasMethod(getId)>', get_class($entity));

if ($entity instanceof Proxy) {
assertType('class-string<Bug4890\Proxy&hasMethod(getId)>', 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<object&hasProperty(myProp)>', get_class($entity));

if ($entity instanceof Proxy) {
assertType('class-string<Bug4890\Proxy&hasProperty(myProp)>', 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));
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/generics.php
Original file line number Diff line number Diff line change
Expand Up @@ -1316,7 +1316,7 @@ function arrayOfGenericClassStrings(array $a): void
function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject)
{
assertType(
'class-string<T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
'class-string<object&T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
get_class($a)
);
assertType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.';
Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-4890b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Bug4890b;

interface Proxy {}

class HelloWorld
{
public function update(object $entity): void
{
assert(method_exists($entity, 'getId'));

$class = $entity instanceof Proxy
? get_parent_class($entity)
: get_class($entity);
assert(is_string($class));
}
}
Loading