Skip to content

Commit a5b2cc8

Browse files
ondrejmirtesclaude
andcommitted
Resolve static in class constant types against the fetched-on class
A class constant's declared type kept `static` bound to the declaring class: `$child::IMPL` with `@var class-string<static>` declared on the parent was `class-string<static(Base)>`, and so was `static::IMPLS` inside the child. Unlike properties and methods, constants had no counterpart of the called-on prototype reflections that resolve `static` against the class the member is accessed on. `static` now becomes the fetched-on type: an object type for `$object::X`, a `static` of the current class for `static::X` and `$this::X`, and a `static<...>` takes its type arguments over to the fetched-on class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011i2xTiyrXgfVkG1tmwMtUM
1 parent f05e6ce commit a5b2cc8

4 files changed

Lines changed: 227 additions & 49 deletions

File tree

‎src/Reflection/InitializerExprTypeResolver.php‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
use PHPStan\Type\FloatType;
7373
use PHPStan\Type\GeneralizePrecision;
7474
use PHPStan\Type\Generic\GenericClassStringType;
75+
use PHPStan\Type\Generic\GenericStaticType;
7576
use PHPStan\Type\Generic\TemplateType;
7677
use PHPStan\Type\Generic\TemplateTypeMap;
7778
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
@@ -94,6 +95,7 @@
9495
use PHPStan\Type\TypeCombinator;
9596
use PHPStan\Type\TypehintHelper;
9697
use PHPStan\Type\TypeResult;
98+
use PHPStan\Type\TypeTraverser;
9799
use PHPStan\Type\TypeUtils;
98100
use PHPStan\Type\TypeWithClassName;
99101
use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
@@ -2734,10 +2736,12 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
27342736
}
27352737

27362738
$types = [];
2737-
foreach ($constantClassType->getObjectClassNames() as $referencedClass) {
2739+
$referencedClasses = $constantClassType->getObjectClassNames();
2740+
foreach ($referencedClasses as $referencedClass) {
27382741
if (!$this->getReflectionProvider()->hasClass($referencedClass)) {
27392742
continue;
27402743
}
2744+
$fetchedOnType = count($referencedClasses) === 1 ? $constantClassType : new ObjectType($referencedClass);
27412745

27422746
$constantClassReflection = $this->getReflectionProvider()->getClass($referencedClass);
27432747
if (!$constantClassReflection->hasConstant($constantName)) {
@@ -2777,13 +2781,13 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
27772781
if ($reflectionConstant->getType() !== null) {
27782782
$nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection);
27792783
}
2780-
$resolvedType = $this->constantResolver->resolveClassConstantType(
2784+
$resolvedType = $this->transformStaticType($this->constantResolver->resolveClassConstantType(
27812785
$constantClassReflection->getName(),
27822786
$constantName,
27832787
$constantType,
27842788
$nativeType,
27852789
$constantClassReflection->getConstantPhpDocType($constantName),
2786-
);
2790+
), $fetchedOnType);
27872791
$this->classConstantValueTypeCache[$resolvingName] = $resolvedType;
27882792
$types[] = $resolvedType;
27892793
unset($this->currentlyResolvingClassConstant[$resolvingName]);
@@ -2808,13 +2812,13 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
28082812
}
28092813

28102814
$nativeType = $constantReflection->getNativeType();
2811-
$constantType = $this->constantResolver->resolveClassConstantType(
2815+
$constantType = $this->transformStaticType($this->constantResolver->resolveClassConstantType(
28122816
$constantClassReflection->getName(),
28132817
$constantName,
28142818
$constantType,
28152819
$nativeType,
28162820
$constantClassReflection->getConstantPhpDocType($constantName),
2817-
);
2821+
), $fetchedOnType);
28182822
unset($this->currentlyResolvingClassConstant[$resolvingName]);
28192823
$types[] = $constantType;
28202824
}
@@ -2827,7 +2831,31 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
28272831
return new ErrorType();
28282832
}
28292833

2830-
return $constantClassType->getConstant($constantName)->getValueType();
2834+
return $this->transformStaticType($constantClassType->getConstant($constantName)->getValueType(), $constantClassType);
2835+
}
2836+
2837+
/**
2838+
* Resolves `static` in the type of a class constant against the class
2839+
* the constant is fetched on.
2840+
*/
2841+
private function transformStaticType(Type $type, Type $fetchedOnType): Type
2842+
{
2843+
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($fetchedOnType): Type {
2844+
if (!$type instanceof StaticType) {
2845+
return $traverse($type);
2846+
}
2847+
if ($fetchedOnType instanceof StaticType) {
2848+
return $traverse($type->changeBaseClass($fetchedOnType->getClassReflection()));
2849+
}
2850+
if ($type instanceof GenericStaticType) {
2851+
$fetchedOnTypeReflections = $fetchedOnType->getObjectClassReflections();
2852+
if (count($fetchedOnTypeReflections) === 1) {
2853+
return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType());
2854+
}
2855+
}
2856+
2857+
return $fetchedOnType;
2858+
});
28312859
}
28322860

28332861
/**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ClassConstantStaticType;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Base
8+
{
9+
10+
/** @var class-string<static> */
11+
const IMPL = Base::class;
12+
13+
/** @var list<class-string<static>> */
14+
const IMPLS = [Base::class];
15+
16+
public function inBase(): void
17+
{
18+
assertType('class-string<static(ClassConstantStaticType\Base)>', static::IMPL);
19+
assertType('class-string<static(ClassConstantStaticType\Base)>', $this::IMPL);
20+
assertType('\'ClassConstantStaticType\\\\Base\'', self::IMPL);
21+
}
22+
23+
}
24+
25+
class Child extends Base
26+
{
27+
28+
public function inChild(): void
29+
{
30+
assertType('class-string<static(ClassConstantStaticType\Child)>', static::IMPL);
31+
assertType('list<class-string<static(ClassConstantStaticType\Child)>>', static::IMPLS);
32+
assertType('class-string<static(ClassConstantStaticType\Child)>', $this::IMPL);
33+
}
34+
35+
}
36+
37+
/**
38+
* @template T
39+
*/
40+
class GenericBase
41+
{
42+
43+
/** @var class-string<static<int>> */
44+
const IMPL = GenericBase::class; // @phpstan-ignore classConstant.phpDocType
45+
46+
}
47+
48+
/** @extends GenericBase<string> */
49+
class GenericChild extends GenericBase
50+
{
51+
52+
}
53+
54+
/**
55+
* @param GenericBase<string> $genericBase
56+
*/
57+
function test(Base $base, Child $child, GenericBase $genericBase, GenericChild $genericChild): void
58+
{
59+
assertType('class-string<ClassConstantStaticType\Base>', $base::IMPL);
60+
assertType('class-string<ClassConstantStaticType\Child>', $child::IMPL);
61+
assertType('list<class-string<ClassConstantStaticType\Child>>', $child::IMPLS);
62+
assertType('class-string<ClassConstantStaticType\GenericBase<int>>', $genericBase::IMPL);
63+
assertType('class-string<ClassConstantStaticType\GenericChild>', $genericChild::IMPL);
64+
}

‎turbo-ext/src/InitializerExprTypeResolver.cpp‎

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ constexpr Method mHasMethod{"hasMethod", PT_OP_HAS_METHOD};
188188
constexpr Method mGetMethod{"getMethod", PT_OP_GET_METHOD};
189189
constexpr Method mIsStatic{"isStatic"};
190190
constexpr Method mGetStaticObjectType{"getStaticObjectType"};
191+
constexpr Method mChangeBaseClass{"changeBaseClass", PT_OP_CHANGE_BASE_CLASS};
192+
constexpr Method mGetClassReflection{"getClassReflection", PT_OP_GET_CLASS_REFLECTION};
193+
constexpr Method mGetObjectClassReflections{"getObjectClassReflections", PT_OP_GET_OBJECT_CLASS_REFLECTIONS};
191194
constexpr Method mGetAncestorWithClassName{"getAncestorWithClassName", PT_OP_GET_ANCESTOR_WITH_CLASS_NAME};
192195
constexpr Method mGetClassName{"getClassName"};
193196
constexpr Method mGetClassStringObjectType{"getClassStringObjectType"};
@@ -4679,6 +4682,15 @@ class InitializerExprTypeResolver
46794682
}
46804683
}
46814684

4685+
/* $fetchedOnType = count($referencedClasses) === 1 ? $constantClassType : new ObjectType($referencedClass) */
4686+
zv::Val fetchedOnType;
4687+
if (zend_hash_num_elements(Z_ARRVAL_P(referencedClasses.raw())) == 1) {
4688+
fetchedOnType = zv::Val::copyOf(zv::Ref(constantClassType.raw()));
4689+
} else {
4690+
fetchedOnType = objectTypeOf(referencedClass);
4691+
if (UNEXPECTED(fetchedOnType.isUndef())) return zv::Val();
4692+
}
4693+
46824694
IETR_VAL(constantClassReflection, reflectionProviderGetClass(referencedClass));
46834695
{
46844696
IETR_VAL(hasConstant, callOn(constantClassReflection.raw(), mHasConstant, 1, constantName));
@@ -4758,7 +4770,8 @@ class InitializerExprTypeResolver
47584770
}
47594771
IETR_VAL(className, classReflectionName(constantClassReflection.raw()));
47604772
IETR_VAL(phpDocType, callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName));
4761-
IETR_VAL(resolvedType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw()));
4773+
IETR_VAL(resolvedClassConstantType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw()));
4774+
IETR_VAL(resolvedType, transformStaticType(resolvedClassConstantType.raw(), fetchedOnType.raw()));
47624775
writeArrayProperty(slots::classConstantValueTypeCache, resolvingName, resolvedType.raw());
47634776
types.push(zv::Ref(resolvedType.raw()));
47644777
unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName);
@@ -4802,7 +4815,8 @@ class InitializerExprTypeResolver
48024815
IETR_VAL(nativeType, callOn(constantReflection.raw(), mGetNativeType));
48034816
IETR_VAL(className, classReflectionName(constantClassReflection.raw()));
48044817
IETR_VAL(phpDocType, callOn(constantClassReflection.raw(), mGetConstantPhpDocType, 1, constantName));
4805-
IETR_VAL(resolvedConstantType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw()));
4818+
IETR_VAL(resolvedClassConstantType, resolveClassConstantType(prop(slots::constantResolver), className.raw(), constantName, constantType.raw(), nativeType.raw(), phpDocType.raw()));
4819+
IETR_VAL(resolvedConstantType, transformStaticType(resolvedClassConstantType.raw(), fetchedOnType.raw()));
48064820
unsetArrayProperty(slots::currentlyResolvingClassConstant, resolvingName);
48074821
types.push(std::move(resolvedConstantType));
48084822
}
@@ -4819,7 +4833,66 @@ class InitializerExprTypeResolver
48194833
}
48204834

48214835
IETR_VAL(constant, callOn(constantClassType.raw(), mGetConstant, 1, constantName));
4822-
return callOn(constant.raw(), mGetValueType);
4836+
IETR_VAL(valueType, callOn(constant.raw(), mGetValueType));
4837+
return transformStaticType(valueType.raw(), constantClassType.raw());
4838+
}
4839+
4840+
/* Mirrors transformStaticType(): TypeTraverser::map($type, the closure
4841+
* below over $fetchedOnType) */
4842+
static zv::Val transformStaticType(zval *type, zval *fetchedOnType)
4843+
{
4844+
IETR_VAL(callback, pt_type_native_callback(transformStaticTypeCallback, fetchedOnType, NULL));
4845+
return pt_type_traverser_map_of(type, callback.raw());
4846+
}
4847+
4848+
/* the closure of transformStaticType(): `static function (Type $type,
4849+
* callable $traverse) use ($fetchedOnType): Type` — a StaticType has its
4850+
* base class changed to the class of a StaticType fetched-on type, or,
4851+
* a GenericStaticType, becomes the static object type of its base class
4852+
* changed to the single fetched-on class; any other StaticType becomes
4853+
* the fetched-on type; everything else traverses */
4854+
static void transformStaticTypeCallback(zval *fetchedOnType, zval *state1, uint32_t argc, zval *argv, zval *return_value)
4855+
{
4856+
(void) state1;
4857+
if (UNEXPECTED(argc < 2)) {
4858+
zend_argument_count_error("Too few arguments to function %s::{closure}(), %u passed and exactly 2 expected", ZSTR_VAL(pt_ce_initializer_expr_type_resolver->name), argc);
4859+
return;
4860+
}
4861+
zval *type = &argv[0];
4862+
zv::Val result = transformStaticTypeStep(type, fetchedOnType, &argv[1]);
4863+
if (UNEXPECTED(result.isUndef())) return;
4864+
result.intoReturnValue(return_value);
4865+
}
4866+
4867+
static zv::Val transformStaticTypeStep(zval *type, zval *fetchedOnType, zval *traverse)
4868+
{
4869+
if (!isA(type, pt_ce_static_type)) {
4870+
return pt_type_call_callable(traverse, 1, type);
4871+
}
4872+
if (isA(fetchedOnType, pt_ce_static_type)) {
4873+
/* return $traverse($type->changeBaseClass($fetchedOnType->getClassReflection())); */
4874+
IETR_VAL(fetchedOnReflection, callOn(fetchedOnType, mGetClassReflection));
4875+
IETR_VAL(changed, callOn(type, mChangeBaseClass, 1, fetchedOnReflection.raw()));
4876+
return pt_type_call_callable(traverse, 1, changed.raw());
4877+
}
4878+
if (isA(type, pt_ce_generic_static_type)) {
4879+
IETR_VAL(reflections, callOn(fetchedOnType, mGetObjectClassReflections));
4880+
if (UNEXPECTED(!requireArray(reflections.raw()))) return zv::Val();
4881+
if (zend_hash_num_elements(Z_ARRVAL_P(reflections.raw())) == 1) {
4882+
/* return $traverse($type->changeBaseClass($fetchedOnTypeReflections[0])->getStaticObjectType()); */
4883+
zval *first = zend_hash_index_find(Z_ARRVAL_P(reflections.raw()), 0);
4884+
if (UNEXPECTED(first == NULL)) {
4885+
zend_throw_error(NULL, "phpstan_turbo: getObjectClassReflections() must return a list");
4886+
return zv::Val();
4887+
}
4888+
ZVAL_DEREF(first);
4889+
IETR_VAL(changed, callOn(type, mChangeBaseClass, 1, first));
4890+
IETR_VAL(staticObjectType, callOn(changed.raw(), mGetStaticObjectType));
4891+
return pt_type_call_callable(traverse, 1, staticObjectType.raw());
4892+
}
4893+
}
4894+
4895+
return zv::Val::copyOf(zv::Ref(fetchedOnType));
48234896
}
48244897

48254898
/* Mirrors getClassConstFetchType(); $className PHP null for null */
@@ -5986,6 +6059,13 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver)
59866059
PT_RETURN_VAL(IETR_THIS.getReflectionProvider());
59876060
});
59886061

6062+
cls.method(sigs::transformStaticType, [](INTERNAL_FUNCTION_PARAMETERS) {
6063+
zval *type, *fetchedOnType;
6064+
if (!zp::parse<zp::Obj, zp::Obj>(execute_data, type, fetchedOnType)) RETURN_THROWS();
6065+
if (UNEXPECTED(!ietrArgType(type, 1) || !ietrArgType(fetchedOnType, 2))) RETURN_THROWS();
6066+
PT_RETURN_VAL(InitializerExprTypeResolver::transformStaticType(type, fetchedOnType));
6067+
});
6068+
59896069
cls.method(sigs::getNeverType, [](INTERNAL_FUNCTION_PARAMETERS) {
59906070
zval *leftType, *rightType;
59916071
if (!zp::parse<zp::Obj, zp::Obj>(execute_data, leftType, rightType)) RETURN_THROWS();

0 commit comments

Comments
 (0)