Skip to content

Commit 01262e4

Browse files
authored
Account for subclasses in get_parent_class() of a $this/static value (#6048)
1 parent 94e684c commit 01262e4

5 files changed

Lines changed: 174 additions & 4 deletions

File tree

‎src/Type/Php/GetParentClassDynamicFunctionReturnTypeExtension.php‎

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
use PHPStan\Type\Constant\ConstantBooleanType;
1313
use PHPStan\Type\Constant\ConstantStringType;
1414
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
15+
use PHPStan\Type\Generic\GenericClassStringType;
16+
use PHPStan\Type\ObjectType;
17+
use PHPStan\Type\StaticType;
1518
use PHPStan\Type\Type;
1619
use PHPStan\Type\TypeCombinator;
1720
use PHPStan\Type\TypeUtils;
@@ -63,9 +66,45 @@ public function getTypeFromFunctionCall(
6366
return TypeCombinator::union(...array_map(fn (ConstantStringType $stringType): Type => $this->findParentClassNameType($stringType->getValue()), $constantStrings));
6467
}
6568

66-
$classNames = $argType->getObjectClassNames();
69+
// A `static::class` string refers to the same late-static-bound type as `$this`/`static`,
70+
// so unwrap it and reuse the object handling below. Non-class-string types resolve to
71+
// an ErrorType here, so they are left alone.
72+
$valueType = $argType;
73+
$classStringObjectType = $argType->getClassStringObjectType();
74+
if ($classStringObjectType instanceof StaticType) {
75+
$valueType = $classStringObjectType;
76+
}
77+
78+
// Inside a trait a late-static-bound value cannot be resolved to a concrete parent,
79+
// same as `$this` above.
80+
if ($scope->isInTrait() && $valueType instanceof StaticType) {
81+
return null;
82+
}
83+
84+
$classNames = $valueType->getObjectClassNames();
6785
if (count($classNames) > 0) {
68-
return TypeCombinator::union(...array_map(fn (string $classNames): Type => $this->findParentClassNameType($classNames), $classNames));
86+
// A `$this`/`static` value can be an instance of a subclass through late static
87+
// binding. For a non-final class the parent class is then not pinned to the declared
88+
// parent: a direct child's parent is the class itself, a deeper descendant's parent is
89+
// some subclass. So the result also includes `class-string<Class>`.
90+
$isLateStaticBound = $valueType instanceof StaticType;
91+
92+
$types = [];
93+
foreach ($classNames as $className) {
94+
$types[] = $this->findParentClassNameType($className);
95+
96+
if (
97+
!$isLateStaticBound
98+
|| !$this->reflectionProvider->hasClass($className)
99+
|| $this->reflectionProvider->getClass($className)->isFinal()
100+
) {
101+
continue;
102+
}
103+
104+
$types[] = new GenericClassStringType(new ObjectType($className));
105+
}
106+
107+
return TypeCombinator::union(...$types);
69108
}
70109

71110
return null;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14951;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class A
8+
{
9+
10+
public function isDirectChildOfA(): bool
11+
{
12+
// $this can be an instance of a subclass, so get_parent_class($this)
13+
// may return A::class (for a direct child) — not just false.
14+
assertType('class-string<Bug14951\A>|false', get_parent_class($this));
15+
16+
// The sibling functions already model late static binding, so they do not have the
17+
// same problem: they keep the subclass possibility instead of pinning to this class.
18+
assertType('class-string<static(Bug14951\A)>', get_called_class());
19+
assertType('class-string<$this(Bug14951\A)>', get_class($this));
20+
21+
// A class-string argument (e.g. self::class) names an exact class, not a runtime
22+
// value, so it keeps the exact parent — no subclass widening.
23+
assertType('false', get_parent_class(self::class));
24+
25+
// static::class is late static bound just like $this, so it is widened the same way.
26+
assertType('class-string<Bug14951\A>|false', get_parent_class(static::class));
27+
28+
return get_parent_class($this) === self::class;
29+
}
30+
31+
}
32+
33+
class B extends A
34+
{
35+
36+
public function parentOfThis(): void
37+
{
38+
assertType('\'Bug14951\\\\A\'|class-string<Bug14951\B>', get_parent_class($this));
39+
// self::class is the exact class B, so its parent is exactly A.
40+
assertType("'Bug14951\\\\A'", get_parent_class(self::class));
41+
// static::class is late static bound, matching get_parent_class($this).
42+
assertType('\'Bug14951\\\\A\'|class-string<Bug14951\B>', get_parent_class(static::class));
43+
}
44+
45+
}
46+
47+
final class FinalNoParent
48+
{
49+
50+
public function parentOfThis(): void
51+
{
52+
// Final class cannot be subclassed, so the parent is exactly false.
53+
assertType('false', get_parent_class($this));
54+
assertType('false', get_parent_class(static::class));
55+
// ... and get_called_class() is pinned to the final class.
56+
assertType("'Bug14951\\\\FinalNoParent'", get_called_class());
57+
}
58+
59+
}
60+
61+
final class FinalWithParent extends A
62+
{
63+
64+
public function parentOfThis(): void
65+
{
66+
assertType("'Bug14951\\\\A'", get_parent_class($this));
67+
assertType("'Bug14951\\\\A'", get_parent_class(static::class));
68+
}
69+
70+
}
71+
72+
/** @final */
73+
class PhpDocFinalNoParent
74+
{
75+
76+
public function parentOfThis(): void
77+
{
78+
// A @final class cannot be subclassed either, so there is no subclass widening.
79+
assertType('false', get_parent_class($this));
80+
assertType('false', get_parent_class(static::class));
81+
}
82+
83+
}
84+
85+
/** @final */
86+
class PhpDocFinalWithParent extends A
87+
{
88+
89+
public function parentOfThis(): void
90+
{
91+
assertType("'Bug14951\\\\A'", get_parent_class($this));
92+
assertType("'Bug14951\\\\A'", get_parent_class(static::class));
93+
}
94+
95+
}

‎tests/PHPStan/Analyser/nsrt/get-parent-class.php‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class Foo
1010
public function doFoo()
1111
{
1212
assertType('false', get_parent_class());
13-
assertType('false', get_parent_class($this));
13+
assertType('class-string<ParentClass\Foo>|false', get_parent_class($this));
1414
assertType('class-string<$this(ParentClass\Foo)>', get_class($this));
1515
assertType('\'ParentClass\\\\Foo\'', get_class());
1616
}
@@ -25,7 +25,7 @@ class Bar extends Foo
2525
public function doBar()
2626
{
2727
assertType('\'ParentClass\\\\Foo\'', get_parent_class());
28-
assertType('\'ParentClass\\\\Foo\'', get_parent_class($this));
28+
assertType('\'ParentClass\\\\Foo\'|class-string<ParentClass\Bar>', get_parent_class($this));
2929
}
3030

3131
}
@@ -45,8 +45,16 @@ trait FooTrait
4545

4646
public function doBaz()
4747
{
48+
// Inside a trait a late-static-bound argument is not resolved to a concrete parent.
4849
assertType('class-string|false', get_parent_class());
4950
assertType('class-string|false', get_parent_class($this));
51+
assertType('class-string|false', get_parent_class(static::class));
52+
53+
// self::class is not late static bound - it is the using class, so it is already a
54+
// constant class-string here, indistinguishable from writing Bar::class.
55+
assertType('\'ParentClass\\\\Bar\'', self::class);
56+
assertType('\'ParentClass\\\\Foo\'', get_parent_class(self::class));
57+
assertType('\'ParentClass\\\\Foo\'', get_parent_class(Bar::class));
5058
}
5159

5260
}

‎tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ public function testBug2835(): void
347347
$this->analyse([__DIR__ . '/data/bug-2835.php'], []);
348348
}
349349

350+
public function testBug14951(): void
351+
{
352+
$this->analyse([__DIR__ . '/data/bug-14951.php'], []);
353+
}
354+
350355
public function testBug1860(): void
351356
{
352357
$this->analyse([__DIR__ . '/data/bug-1860.php'], [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14951StrictComparison;
4+
5+
class A
6+
{
7+
8+
public function isDirectChildOfA(): bool
9+
{
10+
// $this can be an instance of a subclass, so get_parent_class($this)
11+
// can equal self::class for a direct child — this comparison is not always false.
12+
return get_parent_class($this) === self::class;
13+
}
14+
15+
}
16+
17+
class B extends A
18+
{
19+
}
20+
21+
class C extends B
22+
{
23+
}

0 commit comments

Comments
 (0)