Skip to content

Commit 42e1755

Browse files
Improve get_class return type
1 parent 1029bba commit 42e1755

File tree

5 files changed

+106
-23
lines changed

5 files changed

+106
-23
lines changed

src/Type/Php/GetClassDynamicReturnTypeExtension.php

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,12 @@
1414
use PHPStan\Type\Generic\GenericClassStringType;
1515
use PHPStan\Type\Generic\TemplateType;
1616
use PHPStan\Type\IntersectionType;
17-
use PHPStan\Type\MixedType;
17+
use PHPStan\Type\ObjectShapeType;
1818
use PHPStan\Type\ObjectType;
1919
use PHPStan\Type\ObjectWithoutClassType;
2020
use PHPStan\Type\StaticType;
2121
use PHPStan\Type\Type;
22+
use PHPStan\Type\TypeCombinator;
2223
use PHPStan\Type\TypeTraverser;
2324
use PHPStan\Type\TypeUtils;
2425
use PHPStan\Type\UnionType;
@@ -66,30 +67,35 @@ static function (Type $type, callable $traverse): Type {
6667
return new GenericClassStringType(new ObjectType($type->getClassName()));
6768
}
6869

69-
$objectClassNames = $type->getObjectClassNames();
70-
if ($type instanceof TemplateType && $objectClassNames === []) {
71-
if ($type instanceof ObjectWithoutClassType) {
72-
return new GenericClassStringType($type);
73-
}
74-
75-
return new UnionType([
76-
new GenericClassStringType($type),
77-
new ConstantBooleanType(false),
78-
]);
79-
} elseif ($type instanceof MixedType) {
80-
return new UnionType([
81-
new ClassStringType(),
82-
new ConstantBooleanType(false),
83-
]);
84-
} elseif ($type instanceof StaticType) {
85-
return new GenericClassStringType($type->getStaticObjectType());
86-
} elseif ($objectClassNames !== []) {
87-
return new GenericClassStringType($type);
88-
} elseif ($type instanceof ObjectWithoutClassType) {
70+
if ($type instanceof ObjectShapeType) {
8971
return new ClassStringType();
9072
}
9173

92-
return new ConstantBooleanType(false);
74+
$isObject = $type->isObject();
75+
if ($isObject->no()) {
76+
return new ConstantBooleanType(false);
77+
}
78+
79+
if ($type instanceof StaticType) {
80+
$objectType = $type->getStaticObjectType();
81+
} else {
82+
$objectType = TypeCombinator::intersect($type, new ObjectWithoutClassType());
83+
}
84+
85+
if (!$objectType instanceof TemplateType && $objectType instanceof ObjectWithoutClassType) {
86+
$classStringType = new ClassStringType();
87+
} else {
88+
$classStringType = new GenericClassStringType($objectType);
89+
}
90+
91+
if ($isObject->yes()) {
92+
return $classStringType;
93+
}
94+
95+
return new UnionType([
96+
$classStringType,
97+
new ConstantBooleanType(false),
98+
]);
9399
},
94100
);
95101
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
namespace Bug4890;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface Proxy {}
8+
9+
class HelloWorld
10+
{
11+
public function update(object $entity): void
12+
{
13+
assertType('class-string', get_class($entity));
14+
assert(method_exists($entity, 'getId'));
15+
assertType('class-string<object&hasMethod(getId)>', get_class($entity));
16+
17+
if ($entity instanceof Proxy) {
18+
assertType('class-string<Bug4890\Proxy&hasMethod(getId)>', get_class($entity));
19+
}
20+
21+
$class = $entity instanceof Proxy
22+
? get_parent_class($entity)
23+
: get_class($entity);
24+
assert(is_string($class));
25+
26+
}
27+
28+
public function updateProp(object $entity): void
29+
{
30+
assertType('class-string', get_class($entity));
31+
assert(property_exists($entity, 'myProp'));
32+
assertType('class-string<object&hasProperty(myProp)>', get_class($entity));
33+
34+
if ($entity instanceof Proxy) {
35+
assertType('class-string<Bug4890\Proxy&hasProperty(myProp)>', get_class($entity));
36+
}
37+
38+
$class = $entity instanceof Proxy
39+
? get_parent_class($entity)
40+
: get_class($entity);
41+
assert(is_string($class));
42+
}
43+
44+
/**
45+
* @param object{foo: self, bar: int, baz?: string} $entity
46+
*/
47+
public function updateObjectShape($entity): void
48+
{
49+
assertType('class-string', get_class($entity));
50+
assert(property_exists($entity, 'foo'));
51+
assertType('class-string', get_class($entity));
52+
}
53+
}

tests/PHPStan/Analyser/nsrt/generics.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1316,7 +1316,7 @@ function arrayOfGenericClassStrings(array $a): void
13161316
function getClassOnTemplateType($a, $b, $c, $d, $object, $mixed, $tObject)
13171317
{
13181318
assertType(
1319-
'class-string<T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
1319+
'class-string<object&T (function PHPStan\Generics\FunctionsAssertType\getClassOnTemplateType(), argument)>|false',
13201320
get_class($a)
13211321
);
13221322
assertType(

tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,12 @@ public function testLooseComparisonAgainstEnumsNoPhpdoc(): void
927927
$this->analyse([__DIR__ . '/data/loose-comparison-against-enums.php'], $issues);
928928
}
929929

930+
public function testBug4890b(): void
931+
{
932+
$this->treatPhpDocTypesAsCertain = true;
933+
$this->analyse([__DIR__ . '/data/bug-4890b.php'], []);
934+
}
935+
930936
public function testBug10502(): void
931937
{
932938
$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%</>.';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Bug4890b;
4+
5+
interface Proxy {}
6+
7+
class HelloWorld
8+
{
9+
public function update(object $entity): void
10+
{
11+
assert(method_exists($entity, 'getId'));
12+
13+
$class = $entity instanceof Proxy
14+
? get_parent_class($entity)
15+
: get_class($entity);
16+
assert(is_string($class));
17+
}
18+
}

0 commit comments

Comments
 (0)