Skip to content

Commit 0fb1068

Browse files
ondrejmirtesclaude
andcommitted
Give a generic static type a class reflection with its type arguments
GenericStaticType::getStaticObjectType() handed the class reflection of the declaring class, whose template types are unresolved, to the GenericObjectType it builds, and getClassReflection() returned that reflection too. Members of a `static<int, string>` then resolved against `TKey`/`TValue` instead of `int`/`string` whenever the type was used inside the class - `$this->concrete()->all()`, a `@param static<...>` parameter, `static::` calls and properties, `new static(...)` with inferred type arguments, promoted properties typed by the constructor's `@param`, and every `static<...>` of a final class. getClassReflection() now returns the reflection with the type arguments, like GenericObjectType does, and getStaticObjectType() builds on it. getAncestorWithClassName() keeps the type arguments the static object type has for the ancestor class: through changeBaseClass(), which can only carry them down to a subclass, an ancestor got its template bounds instead. This replaces the workaround in CalledOnTypeUnresolvedMethodPrototypeReflection that rebuilt the object type without the reflection for method calls from outside the class (#6695); its tests are kept. Three expectations pinned the bug: - bug-4590: `return new static(["ok" => "hello"])` from a method returning `static<array<string, string>>` now infers `T` from the return type, like `return new OkResponse([...])` in the same file already did. A real mismatch is added instead. - pr-5880: after `@phpstan-assert-if-true static<A> $this`, `$this->get()` is `A` and `->a()` exists. - bug-12575: after `@phpstan-self-out static<T&U>`, `$this->getT()` is `A&T`. The native GenericStaticType declares its properties from the generated declarations now, so the new memo property keeps the slots in step with the PHP twin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i2xTiyrXgfVkG1tmwMtUM
1 parent acd0511 commit 0fb1068

11 files changed

Lines changed: 539 additions & 99 deletions

‎src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php‎

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use PHPStan\Reflection\Php\ExtendedDummyParameter;
1212
use PHPStan\Reflection\ResolvedMethodReflection;
1313
use PHPStan\Turbo\ShadowedByTurboExtension;
14-
use PHPStan\Type\Generic\GenericObjectType;
1514
use PHPStan\Type\Generic\GenericStaticType;
1615
use PHPStan\Type\StaticType;
1716
use PHPStan\Type\ThisType;
@@ -150,19 +149,7 @@ private function transformStaticType(Type $type): Type
150149
if (count($calledOnTypeReflections) === 1) {
151150
$calledOnTypeReflection = $calledOnTypeReflections[0];
152151

153-
$staticObjectType = $type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType();
154-
if ($staticObjectType instanceof GenericObjectType) {
155-
// do not carry over the class reflection of the declaring class,
156-
// it would not know the type arguments of the resolved type
157-
$staticObjectType = new GenericObjectType(
158-
$staticObjectType->getClassName(),
159-
$staticObjectType->getTypes(),
160-
$staticObjectType->getSubtractedType(),
161-
variances: $staticObjectType->getVariances(),
162-
);
163-
}
164-
165-
return $traverse($staticObjectType);
152+
return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType());
166153
}
167154

168155
return $this->calledOnType;

‎src/Type/Generic/GenericStaticType.php‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ class GenericStaticType extends StaticType
2828

2929
private ?ObjectType $staticObjectType = null;
3030

31+
private ?ClassReflection $resolvedClassReflection = null;
32+
3133
/**
3234
* @api
3335
* @param array<int, Type> $types
@@ -60,6 +62,12 @@ public function getVariances(): array
6062
return $this->variances;
6163
}
6264

65+
/** The class reflection with the type arguments of this type. */
66+
public function getClassReflection(): ClassReflection
67+
{
68+
return $this->resolvedClassReflection ??= $this->classReflection->withTypes($this->types)->withVariances($this->variances);
69+
}
70+
6371
public function getStaticObjectType(): ObjectType
6472
{
6573
if ($this->staticObjectType === null) {
@@ -68,7 +76,7 @@ public function getStaticObjectType(): ObjectType
6876
$this->classReflection->getName(),
6977
$this->types,
7078
$this->subtractedType,
71-
$this->classReflection,
79+
$this->getClassReflection(),
7280
$this->variances,
7381
);
7482
}
@@ -79,6 +87,34 @@ public function getStaticObjectType(): ObjectType
7987
return $this->staticObjectType;
8088
}
8189

90+
/**
91+
* The ancestor keeps the type arguments the static object type has for
92+
* that class - changeBaseClass() can only carry them down to a subclass.
93+
*/
94+
public function getAncestorWithClassName(string $className): ?TypeWithClassName
95+
{
96+
$ancestor = $this->getStaticObjectType()->getAncestorWithClassName($className);
97+
if ($ancestor === null) {
98+
return null;
99+
}
100+
101+
$classReflection = $ancestor->getClassReflection();
102+
if ($classReflection === null) {
103+
return null;
104+
}
105+
106+
if ($classReflection->getName() === $this->getClassName() || !$classReflection->isGeneric()) {
107+
return $this->changeBaseClass($classReflection);
108+
}
109+
110+
return new self(
111+
$classReflection,
112+
$classReflection->typeMapToList($classReflection->getActiveTemplateTypeMap()),
113+
$this->subtractedType,
114+
$classReflection->varianceMapToList($classReflection->getCallSiteVarianceMap()),
115+
);
116+
}
117+
82118
public function changeBaseClass(ClassReflection $classReflection): StaticType
83119
{
84120
if ($classReflection->getName() === $this->getClassName()) {

‎tests/PHPStan/Analyser/AnalyserIntegrationTest.php‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,8 +1714,7 @@ public function testPr5880(): void
17141714
{
17151715
// endless loop
17161716
$errors = $this->runAnalyse(__DIR__ . '/data/pr-5880.php');
1717-
$this->assertCount(1, $errors);
1718-
$this->assertSame('Call to an undefined method (T of PR5880EndlessRecursion\A = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B)|T of PR5880EndlessRecursion\B = PR5880EndlessRecursion\A|PR5880EndlessRecursion\B::a().', $errors[0]->getMessage());
1717+
$this->assertNoErrors($errors);
17191718
}
17201719

17211720
public function testBug14707(): void

‎tests/PHPStan/Analyser/nsrt/bug-12575.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,14 @@ public function doFoo(): void
3434
{
3535
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\A&T of object (class Bug12575\Bar, argument)>)', $this->add(A::class));
3636
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\A&T of object (class Bug12575\Bar, argument)>)', $this);
37-
assertType('T of object (class Bug12575\Bar, parameter)', $this->getT());
37+
assertType('Bug12575\A&T of object (class Bug12575\Bar, argument)', $this->getT());
3838
}
3939

4040
public function doBar(): void
4141
{
4242
$this->add(B::class);
4343
assertType('$this(Bug12575\Bar<T of object (class Bug12575\Bar, argument)>)&static(Bug12575\Bar<Bug12575\B&T of object (class Bug12575\Bar, argument)>)', $this);
44-
assertType('T of object (class Bug12575\Bar, parameter)', $this->getT());
44+
assertType('Bug12575\B&T of object (class Bug12575\Bar, argument)', $this->getT());
4545
}
4646

4747
/**
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace GenericStaticInClassPromoted;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
class ANode
10+
{
11+
12+
}
13+
14+
class BNode
15+
{
16+
17+
}
18+
19+
/**
20+
* @template TNode of object
21+
*/
22+
class NodeCursor
23+
{
24+
25+
/** @var static<ANode> */
26+
public ?self $a = null;
27+
28+
/** @var static<ANode> */
29+
public static ?self $staticA = null;
30+
31+
/** @param TNode $node */
32+
final public function __construct(public object $node)
33+
{
34+
}
35+
36+
public function inside(BNode $b): void
37+
{
38+
assertType('GenericStaticInClassPromoted\ANode|null', $this->a?->node);
39+
assertType('GenericStaticInClassPromoted\ANode|null', static::$staticA?->node);
40+
41+
$static = new static($b);
42+
assertType('GenericStaticInClassPromoted\BNode', $static->node);
43+
assertType('GenericStaticInClassPromoted\ANode|null', $static->a?->node);
44+
45+
$self = new self($b);
46+
assertType('GenericStaticInClassPromoted\BNode', $self->node);
47+
assertType('GenericStaticInClassPromoted\ANode|null', $self->a?->node);
48+
}
49+
50+
}
51+
52+
/**
53+
* @template TNode of object
54+
*/
55+
final class FinalNodeCursor
56+
{
57+
58+
/** @var static<ANode> */
59+
public ?self $a = null;
60+
61+
/** @param TNode $node */
62+
public function __construct(public object $node)
63+
{
64+
}
65+
66+
public function inside(BNode $b): void
67+
{
68+
assertType('GenericStaticInClassPromoted\ANode|null', $this->a?->node);
69+
70+
$static = new static($b);
71+
assertType('GenericStaticInClassPromoted\BNode', $static->node);
72+
}
73+
74+
}

0 commit comments

Comments
 (0)