Skip to content

Commit 1cb6abf

Browse files
pawell67claude
andauthored
Resolve template types of method calls on a concrete generic static return type (#6695)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
1 parent 87bbf77 commit 1cb6abf

4 files changed

Lines changed: 124 additions & 1 deletion

File tree

‎src/Reflection/Type/CalledOnTypeUnresolvedMethodPrototypeReflection.php‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Reflection\Php\ExtendedDummyParameter;
1212
use PHPStan\Reflection\ResolvedMethodReflection;
1313
use PHPStan\Turbo\ShadowedByTurboExtension;
14+
use PHPStan\Type\Generic\GenericObjectType;
1415
use PHPStan\Type\Generic\GenericStaticType;
1516
use PHPStan\Type\StaticType;
1617
use PHPStan\Type\ThisType;
@@ -149,7 +150,19 @@ private function transformStaticType(Type $type): Type
149150
if (count($calledOnTypeReflections) === 1) {
150151
$calledOnTypeReflection = $calledOnTypeReflections[0];
151152

152-
return $traverse($type->changeBaseClass($calledOnTypeReflection)->getStaticObjectType());
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);
153166
}
154167

155168
return $this->calledOnType;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug12680;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @template TKey of array-key
9+
* @template TValue
10+
*/
11+
class Collection
12+
{
13+
14+
/** @return static<array-key, mixed> */
15+
public function pluck(): static
16+
{
17+
return new static(); // @phpstan-ignore new.static
18+
}
19+
20+
/** @return self<array-key, mixed> */
21+
public function pluckSelf(): static // @phpstan-ignore return.type
22+
{
23+
return new static(); // @phpstan-ignore new.static
24+
}
25+
26+
/** @return TValue */
27+
public function first() // @phpstan-ignore missingType.return
28+
{
29+
}
30+
31+
}
32+
33+
function (Collection $c): void {
34+
assertType('mixed', $c->pluck()->first());
35+
assertType('mixed', $c->pluckSelf()->first());
36+
assertType('mixed', (new Collection())->pluck()->first());
37+
assertType('mixed', (new Collection())->pluckSelf()->first());
38+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace GenericStaticConcreteArguments;
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+
/** @return static<int, string> */
15+
public function concrete()
16+
{
17+
return new static();
18+
}
19+
20+
/** @return static<int, TValue> */
21+
public function withTemplate()
22+
{
23+
return new static();
24+
}
25+
26+
/** @return array<TKey, TValue> */
27+
public function all(): array
28+
{
29+
return [];
30+
}
31+
32+
/** @return TValue */
33+
public function first()
34+
{
35+
return 1;
36+
}
37+
38+
}
39+
40+
/**
41+
* @param Box<string, bool> $box
42+
*/
43+
function test(Box $box): void
44+
{
45+
assertType('GenericStaticConcreteArguments\Box<int, string>', $box->concrete());
46+
assertType('array<int, string>', $box->concrete()->all());
47+
assertType('string', $box->concrete()->first());
48+
assertType('array<int, bool>', $box->withTemplate()->all());
49+
}

‎turbo-ext/src/CalledOnTypeUnresolvedMethodPrototypeReflection.cpp‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,29 @@ class CalledOnTypeUnresolvedMethodPrototypeReflection
222222
}
223223
zv::Val staticObjectType = pt_type_call(Z_OBJ_P(changed.raw()), PT_LC("getstaticobjecttype"), 0, NULL);
224224
if (UNEXPECTED(staticObjectType.isUndef())) return;
225+
/* a GenericObjectType is rebuilt without the class reflection of the
226+
* declaring class, which would not know the resolved type arguments:
227+
* new GenericObjectType($t->getClassName(), $t->getTypes(), $t->getSubtractedType(), variances: $t->getVariances()) */
228+
bool isGenericObject;
229+
pt_type_instanceof_ce(staticObjectType.raw(), pt_ce_generic_object_type, isGenericObject);
230+
if (isGenericObject) {
231+
zend_object *objectType = Z_OBJ_P(staticObjectType.raw());
232+
zv::Val className = pt_type_call(objectType, PT_LC("getclassname"), 0, NULL);
233+
if (UNEXPECTED(className.isUndef())) return;
234+
zv::Val objectTypes = pt_type_call(objectType, PT_LC("gettypes"), 0, NULL);
235+
if (UNEXPECTED(objectTypes.isUndef())) return;
236+
zv::Val subtracted = pt_type_call(objectType, PT_LC("getsubtractedtype"), 0, NULL);
237+
if (UNEXPECTED(subtracted.isUndef())) return;
238+
zv::Val objectVariances = pt_type_call(objectType, PT_LC("getvariances"), 0, NULL);
239+
if (UNEXPECTED(objectVariances.isUndef())) return;
240+
if (UNEXPECTED(Z_TYPE_P(className.raw()) != IS_STRING)) {
241+
zend_type_error("phpstan_turbo: getClassName() must return a string");
242+
return;
243+
}
244+
zval rebuiltRaw;
245+
if (UNEXPECTED(!pt_generic_object_type_new(&rebuiltRaw, Z_STR_P(className.raw()), objectTypes.raw(), subtracted.raw(), NULL, objectVariances.raw()))) return;
246+
staticObjectType = zv::Val::adopt(rebuiltRaw);
247+
}
225248
zv::Val traversed = pt_type_call_callable(&argv[1], 1, staticObjectType.raw());
226249
if (UNEXPECTED(traversed.isUndef())) return;
227250
traversed.intoReturnValue(return_value);

0 commit comments

Comments
 (0)