Skip to content

Commit f05e6ce

Browse files
ondrejmirtesHawkForgeclaude
committed
Resolve generic static<...> property types against the fetched-on class
CalledOnTypeUnresolvedPropertyPrototypeReflection replaced every StaticType, GenericStaticType included, with the fetched-on type, so the type arguments written in `@var static<...>` were dropped: on `NodeCursor<BNode>`, a property declared `static<ANode>` was read as `NodeCursor<BNode>`. This handles GenericStaticType like CalledOnTypeUnresolvedMethodPrototypeReflection does for method return types. The bug-15378 regression test comes from #6799. Co-Authored-By: HawkForge <75696486+HawkForge@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i2xTiyrXgfVkG1tmwMtUM
1 parent 0fb1068 commit f05e6ce

4 files changed

Lines changed: 157 additions & 2 deletions

File tree

‎src/Reflection/Type/CalledOnTypeUnresolvedPropertyPrototypeReflection.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
use PHPStan\Reflection\ExtendedPropertyReflection;
88
use PHPStan\Reflection\ResolvedPropertyReflection;
99
use PHPStan\Turbo\ShadowedByTurboExtension;
10+
use PHPStan\Type\Generic\GenericStaticType;
1011
use PHPStan\Type\StaticType;
1112
use PHPStan\Type\Type;
1213
use PHPStan\Type\TypeTraverser;
14+
use function count;
1315

1416
#[ShadowedByTurboExtension(implementation: __DIR__ . '/../../../turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp')]
1517
final class CalledOnTypeUnresolvedPropertyPrototypeReflection implements UnresolvedPropertyPrototypeReflection
@@ -85,6 +87,14 @@ private function transformPropertyWithStaticType(ClassReflection $declaringClass
8587
private function transformStaticType(Type $type): Type
8688
{
8789
return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
90+
if ($type instanceof GenericStaticType) {
91+
$fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections();
92+
if (count($fetchedOnTypeReflections) === 1) {
93+
return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType());
94+
}
95+
96+
return $this->fetchedOnType;
97+
}
8898
if ($type instanceof StaticType) {
8999
return $this->fetchedOnType;
90100
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php // lint >= 8.1
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15378;
6+
7+
use function PHPStan\Testing\assertType;
8+
9+
/**
10+
* @template TNode of object
11+
*/
12+
class NodeCursor {
13+
/**
14+
* @var static<ANode>
15+
*/
16+
public ?self $a = null;
17+
18+
/**
19+
* @var static
20+
*/
21+
public ?self $b = null;
22+
23+
final public function __construct(
24+
/**
25+
* @var TNode
26+
*/
27+
public readonly object $node,
28+
) {
29+
// empty
30+
}
31+
}
32+
33+
class ANode {
34+
35+
}
36+
37+
class BNode {
38+
39+
}
40+
41+
$node = new BNode();
42+
$cursor = new NodeCursor($node);
43+
44+
assertType('Bug15378\NodeCursor<Bug15378\ANode>|null', $cursor->a);
45+
assertType('Bug15378\NodeCursor<Bug15378\BNode>|null', $cursor->b);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GenericStaticProperty;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TValue
10+
*/
11+
class Box
12+
{
13+
14+
/** @var static<int, string> */
15+
public $prop;
16+
17+
/** @var static<int, TValue> */
18+
public $withTemplate;
19+
20+
/** @var static<int, string> */
21+
public static $staticProp;
22+
23+
/** @return array<TKey, TValue> */
24+
public function all(): array
25+
{
26+
return [];
27+
}
28+
29+
}
30+
31+
/**
32+
* @template TKey of array-key
33+
* @template TValue
34+
*/
35+
final class FinalBox
36+
{
37+
38+
/** @var static<int, string> */
39+
public $prop;
40+
41+
/** @return array<TKey, TValue> */
42+
public function all(): array
43+
{
44+
return [];
45+
}
46+
47+
}
48+
49+
/**
50+
* @param Box<string, bool> $box
51+
* @param FinalBox<string, bool> $finalBox
52+
*/
53+
function test(Box $box, FinalBox $finalBox): void
54+
{
55+
assertType('GenericStaticProperty\Box<int, string>', $box->prop);
56+
assertType('array<int, string>', $box->prop->all());
57+
assertType('array<int, bool>', $box->withTemplate->all());
58+
assertType('array<int, string>', Box::$staticProp->all());
59+
assertType('array<int, string>', $box::$staticProp->all());
60+
assertType('GenericStaticProperty\FinalBox<int, string>', $finalBox->prop);
61+
assertType('array<int, string>', $finalBox->prop->all());
62+
}

‎turbo-ext/src/CalledOnTypeUnresolvedPropertyPrototypeReflection.cpp‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,10 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection
144144
}
145145

146146
/* the closure of transformStaticType(): `function (Type $type, callable
147-
* $traverse): Type` — a StaticType becomes the fetched-on type,
148-
* everything else traverses */
147+
* $traverse): Type` — a GenericStaticType becomes the static object
148+
* type of its base class changed to the single fetched-on class
149+
* (traversed on), or the fetched-on type itself; any other StaticType
150+
* the fetched-on type; everything else traverses */
149151
static void traverse(zval *thisZv, zval *state1, uint32_t argc, zval *argv, zval *return_value)
150152
{
151153
(void) state1;
@@ -158,6 +160,42 @@ class CalledOnTypeUnresolvedPropertyPrototypeReflection
158160
return;
159161
}
160162
zval *type = &argv[0];
163+
bool isGenericStatic;
164+
pt_type_instanceof_ce(type, pt_ce_generic_static_type, isGenericStatic);
165+
if (isGenericStatic) {
166+
zval *fetchedOnType = CalledOnTypeUnresolvedPropertyPrototypeReflection(Z_OBJ_P(thisZv)).slot(slots::fetchedOnType, "fetchedOnType");
167+
if (UNEXPECTED(fetchedOnType == NULL)) return;
168+
/* $fetchedOnTypeReflections = $this->fetchedOnType->getObjectClassReflections(); */
169+
zv::Val reflections = pt_type_call(Z_OBJ_P(fetchedOnType), PT_LC("getobjectclassreflections"), 0, NULL);
170+
if (UNEXPECTED(reflections.isUndef())) return;
171+
if (UNEXPECTED(!zv::Ref(reflections.raw()).isArray())) {
172+
zend_type_error("phpstan_turbo: getObjectClassReflections() must return an array");
173+
return;
174+
}
175+
if (zend_hash_num_elements(Z_ARRVAL_P(reflections.raw())) == 1) {
176+
/* return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType()); */
177+
zval *first = zend_hash_index_find(Z_ARRVAL_P(reflections.raw()), 0);
178+
if (UNEXPECTED(first == NULL)) {
179+
zend_throw_error(NULL, "phpstan_turbo: getObjectClassReflections() must return a list");
180+
return;
181+
}
182+
ZVAL_DEREF(first);
183+
zv::Val changed = pt_type_call(Z_OBJ_P(type), PT_LC("changebaseclass"), 1, first);
184+
if (UNEXPECTED(changed.isUndef())) return;
185+
if (UNEXPECTED(!zv::Ref(changed.raw()).isObject())) {
186+
zend_type_error("phpstan_turbo: changeBaseClass() must return an object");
187+
return;
188+
}
189+
zv::Val staticObjectType = pt_type_call(Z_OBJ_P(changed.raw()), PT_LC("getstaticobjecttype"), 0, NULL);
190+
if (UNEXPECTED(staticObjectType.isUndef())) return;
191+
zv::Val traversed = pt_type_call_callable(&argv[1], 1, staticObjectType.raw());
192+
if (UNEXPECTED(traversed.isUndef())) return;
193+
traversed.intoReturnValue(return_value);
194+
return;
195+
}
196+
ZVAL_COPY(return_value, fetchedOnType);
197+
return;
198+
}
161199
bool isStatic;
162200
pt_type_instanceof_ce(type, pt_ce_static_type, isStatic);
163201
if (isStatic) {

0 commit comments

Comments
 (0)