Skip to content

Commit 44ef0cd

Browse files
VincentLangletclaude
authored andcommitted
Narrow ReflectionClass::getConstant() and ReflectionClass::getConstants() return types based on generic parameter
- Add `ReflectionClassGetConstantsDynamicReturnTypeExtension` that narrows return types when the `T` template parameter of `ReflectionClass<T>` is known - `getConstant('NAME')` returns the exact constant value type when the constant exists, `false` when it doesn't, or a union of all constant types | false for dynamic names - `getConstants()` returns a constant array shape with all class constant names and their value types - `getConstants($filter)` respects visibility filter parameter (IS_PUBLIC/IS_PROTECTED/IS_PRIVATE) - Enum cases are handled as `EnumCaseObjectType`, regular constants use their value types - Inherited constants and interface constants are included - Falls back to default `mixed`/`array<string, mixed>` when the generic parameter is unknown Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 73dae21 commit 44ef0cd

2 files changed

Lines changed: 357 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\MethodCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\DependencyInjection\AutowiredService;
8+
use PHPStan\Reflection\ClassReflection;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\Constant\ConstantStringType;
13+
use PHPStan\Type\DynamicMethodReturnTypeExtension;
14+
use PHPStan\Type\Enum\EnumCaseObjectType;
15+
use PHPStan\Type\ObjectWithoutClassType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
use ReflectionClass;
19+
use function count;
20+
use function is_int;
21+
22+
#[AutowiredService]
23+
final class ReflectionClassGetConstantsDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
24+
{
25+
26+
public function getClass(): string
27+
{
28+
return ReflectionClass::class;
29+
}
30+
31+
public function isMethodSupported(MethodReflection $methodReflection): bool
32+
{
33+
return $methodReflection->getName() === 'getConstant'
34+
|| $methodReflection->getName() === 'getConstants';
35+
}
36+
37+
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): ?Type
38+
{
39+
$calledOnType = $scope->getType($methodCall->var);
40+
$reflectionType = $calledOnType->getTemplateType(ReflectionClass::class, 'T');
41+
42+
if ((new ObjectWithoutClassType())->isSuperTypeOf($reflectionType)->no()) {
43+
return null;
44+
}
45+
46+
$classReflections = $reflectionType->getObjectClassReflections();
47+
if (count($classReflections) === 0) {
48+
return null;
49+
}
50+
51+
if ($methodReflection->getName() === 'getConstant') {
52+
return $this->resolveGetConstant($methodCall, $scope, $classReflections);
53+
}
54+
55+
$filterType = count($methodCall->getArgs()) >= 1
56+
? $scope->getType($methodCall->getArgs()[0]->value)
57+
: null;
58+
59+
return $this->resolveGetConstants($classReflections, $filterType);
60+
}
61+
62+
/**
63+
* @param list<ClassReflection> $classReflections
64+
*/
65+
private function resolveGetConstant(MethodCall $methodCall, Scope $scope, array $classReflections): ?Type
66+
{
67+
if (count($methodCall->getArgs()) < 1) {
68+
return null;
69+
}
70+
71+
$nameType = $scope->getType($methodCall->getArgs()[0]->value);
72+
$constantNames = $nameType->getConstantStrings();
73+
74+
if (count($constantNames) > 0) {
75+
$types = [];
76+
foreach ($classReflections as $classReflection) {
77+
foreach ($constantNames as $constantName) {
78+
$name = $constantName->getValue();
79+
if ($classReflection->isEnum() && $classReflection->hasEnumCase($name)) {
80+
$types[] = new EnumCaseObjectType($classReflection->getName(), $name);
81+
} elseif ($classReflection->hasConstant($name)) {
82+
$types[] = $classReflection->getConstant($name)->getValueType();
83+
} else {
84+
$types[] = new ConstantBooleanType(false);
85+
}
86+
}
87+
}
88+
89+
if (count($types) === 0) {
90+
return null;
91+
}
92+
93+
return TypeCombinator::union(...$types);
94+
}
95+
96+
$allConstantTypes = [];
97+
foreach ($classReflections as $classReflection) {
98+
foreach ($this->getClassConstants($classReflection) as [$name, $valueType]) {
99+
$allConstantTypes[] = $valueType;
100+
}
101+
}
102+
103+
if (count($allConstantTypes) === 0) {
104+
return new ConstantBooleanType(false);
105+
}
106+
107+
$allConstantTypes[] = new ConstantBooleanType(false);
108+
109+
return TypeCombinator::union(...$allConstantTypes);
110+
}
111+
112+
/**
113+
* @param list<ClassReflection> $classReflections
114+
*/
115+
private function resolveGetConstants(array $classReflections, ?Type $filterType): ?Type
116+
{
117+
$filter = null;
118+
if ($filterType !== null) {
119+
$filterScalars = $filterType->getConstantScalarValues();
120+
if (count($filterScalars) === 1 && is_int($filterScalars[0])) {
121+
$filter = $filterScalars[0];
122+
}
123+
}
124+
125+
$types = [];
126+
foreach ($classReflections as $classReflection) {
127+
$builder = ConstantArrayTypeBuilder::createEmpty();
128+
foreach ($this->getClassConstants($classReflection, $filter) as [$name, $valueType]) {
129+
$builder->setOffsetValueType(new ConstantStringType($name), $valueType);
130+
}
131+
$types[] = $builder->getArray();
132+
}
133+
134+
if (count($types) === 0) {
135+
return null;
136+
}
137+
138+
return TypeCombinator::union(...$types);
139+
}
140+
141+
/**
142+
* @return list<array{string, Type}>
143+
*/
144+
private function getClassConstants(ClassReflection $classReflection, ?int $filter = null): array
145+
{
146+
$constants = [];
147+
foreach ($classReflection->getNativeReflection()->getReflectionConstants() as $reflectionConstant) {
148+
$constantName = $reflectionConstant->getName();
149+
150+
if ($filter !== null && ($reflectionConstant->getModifiers() & $filter) === 0) {
151+
continue;
152+
}
153+
154+
if ($classReflection->isEnum() && $classReflection->hasEnumCase($constantName)) {
155+
$constants[] = [$constantName, new EnumCaseObjectType($classReflection->getName(), $constantName)];
156+
continue;
157+
}
158+
159+
if (!$classReflection->hasConstant($constantName)) {
160+
continue;
161+
}
162+
163+
$constants[] = [$constantName, $classReflection->getConstant($constantName)->getValueType()];
164+
}
165+
166+
return $constants;
167+
}
168+
169+
}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<?php // lint >= 8.1
2+
3+
namespace ReflectionClassGetConstants;
4+
5+
use ReflectionClass;
6+
use function PHPStan\Testing\assertType;
7+
8+
class Foo
9+
{
10+
public const A = 1;
11+
public const B = 'hello';
12+
protected const C = 3.14;
13+
private const D = true;
14+
}
15+
16+
class Bar
17+
{
18+
public const X = 'x';
19+
}
20+
21+
final class FinalClass
22+
{
23+
public const ONE = 1;
24+
public const TWO = 2;
25+
}
26+
27+
enum SimpleEnum
28+
{
29+
case Hearts;
30+
case Diamonds;
31+
}
32+
33+
enum BackedEnum: string
34+
{
35+
case Active = 'active';
36+
case Inactive = 'inactive';
37+
}
38+
39+
enum MixedEnum: int
40+
{
41+
const SOME_CONST = 42;
42+
case One = 1;
43+
case Two = 2;
44+
}
45+
46+
interface HasConstants
47+
{
48+
public const IFACE_CONST = 'iface';
49+
}
50+
51+
class ParentClass
52+
{
53+
public const PARENT_CONST = 'parent';
54+
}
55+
56+
class ChildClass extends ParentClass
57+
{
58+
public const CHILD_CONST = 'child';
59+
}
60+
61+
/**
62+
* @param ReflectionClass<Foo> $ref
63+
*/
64+
function testGetConstantKnown(ReflectionClass $ref): void
65+
{
66+
assertType('1', $ref->getConstant('A'));
67+
assertType("'hello'", $ref->getConstant('B'));
68+
assertType('3.14', $ref->getConstant('C'));
69+
assertType('true', $ref->getConstant('D'));
70+
}
71+
72+
/**
73+
* @param ReflectionClass<Foo> $ref
74+
*/
75+
function testGetConstantNonExistent(ReflectionClass $ref): void
76+
{
77+
assertType('false', $ref->getConstant('nonExistent'));
78+
}
79+
80+
/**
81+
* @param ReflectionClass<Foo> $ref
82+
*/
83+
function testGetConstantDynamic(ReflectionClass $ref, string $name): void
84+
{
85+
assertType("1|3.14|'hello'|bool", $ref->getConstant($name));
86+
}
87+
88+
function testGetConstantUnknownClass(ReflectionClass $ref): void
89+
{
90+
assertType('mixed', $ref->getConstant('A'));
91+
}
92+
93+
/**
94+
* @param ReflectionClass<Foo> $ref
95+
*/
96+
function testGetConstants(ReflectionClass $ref): void
97+
{
98+
assertType("array{A: 1, B: 'hello', C: 3.14, D: true}", $ref->getConstants());
99+
}
100+
101+
function testGetConstantsUnknownClass(ReflectionClass $ref): void
102+
{
103+
assertType('array<string, mixed>', $ref->getConstants());
104+
}
105+
106+
/**
107+
* @param ReflectionClass<FinalClass> $ref
108+
*/
109+
function testGetConstantsFinalClass(ReflectionClass $ref): void
110+
{
111+
assertType('array{ONE: 1, TWO: 2}', $ref->getConstants());
112+
}
113+
114+
/**
115+
* @param ReflectionClass<Bar> $ref
116+
*/
117+
function testGetConstantsSimple(ReflectionClass $ref): void
118+
{
119+
assertType("array{X: 'x'}", $ref->getConstants());
120+
}
121+
122+
/**
123+
* @param ReflectionClass<SimpleEnum> $ref
124+
*/
125+
function testGetConstantsEnum(ReflectionClass $ref): void
126+
{
127+
assertType('array{Hearts: ReflectionClassGetConstants\SimpleEnum::Hearts, Diamonds: ReflectionClassGetConstants\SimpleEnum::Diamonds}', $ref->getConstants());
128+
}
129+
130+
/**
131+
* @param ReflectionClass<BackedEnum> $ref
132+
*/
133+
function testGetConstantsBackedEnum(ReflectionClass $ref): void
134+
{
135+
assertType('array{Active: ReflectionClassGetConstants\BackedEnum::Active, Inactive: ReflectionClassGetConstants\BackedEnum::Inactive}', $ref->getConstants());
136+
}
137+
138+
/**
139+
* @param ReflectionClass<MixedEnum> $ref
140+
*/
141+
function testGetConstantsEnumWithConst(ReflectionClass $ref): void
142+
{
143+
assertType('array{SOME_CONST: 42, One: ReflectionClassGetConstants\MixedEnum::One, Two: ReflectionClassGetConstants\MixedEnum::Two}', $ref->getConstants());
144+
}
145+
146+
/**
147+
* @param ReflectionClass<SimpleEnum> $ref
148+
*/
149+
function testGetConstantEnumCase(ReflectionClass $ref): void
150+
{
151+
assertType('ReflectionClassGetConstants\SimpleEnum::Hearts', $ref->getConstant('Hearts'));
152+
assertType('false', $ref->getConstant('nonExistent'));
153+
}
154+
155+
/**
156+
* @param ReflectionClass<HasConstants> $ref
157+
*/
158+
function testGetConstantsInterface(ReflectionClass $ref): void
159+
{
160+
assertType("array{IFACE_CONST: 'iface'}", $ref->getConstants());
161+
}
162+
163+
/**
164+
* @param ReflectionClass<ChildClass> $ref
165+
*/
166+
function testGetConstantsInheritance(ReflectionClass $ref): void
167+
{
168+
assertType("array{CHILD_CONST: 'child', PARENT_CONST: 'parent'}", $ref->getConstants());
169+
}
170+
171+
/**
172+
* @param ReflectionClass<ChildClass> $ref
173+
*/
174+
function testGetConstantInherited(ReflectionClass $ref): void
175+
{
176+
assertType("'parent'", $ref->getConstant('PARENT_CONST'));
177+
assertType("'child'", $ref->getConstant('CHILD_CONST'));
178+
}
179+
180+
/**
181+
* @param ReflectionClass<Foo> $ref
182+
*/
183+
function testGetConstantsWithFilter(ReflectionClass $ref): void
184+
{
185+
assertType("array{A: 1, B: 'hello'}", $ref->getConstants(\ReflectionClassConstant::IS_PUBLIC));
186+
assertType('array{C: 3.14}', $ref->getConstants(\ReflectionClassConstant::IS_PROTECTED));
187+
assertType('array{D: true}', $ref->getConstants(\ReflectionClassConstant::IS_PRIVATE));
188+
}

0 commit comments

Comments
 (0)