Skip to content

Commit 8fafec7

Browse files
committed
Extract Type::toClassConstantType() for $x::class
Replaces `InitializerExprTypeResolver::getClassConstFetchTypeByReflection()`'s hand-rolled `TypeTraverser::map` for the `::class` constant projection with a polymorphic `Type` method. Per leaf: - `NullType` (incl. `TemplateNullType` via `isNull()` check): pass through (`null::class` reads as `null` in PHPStan's modeling). - Definite-non-object types (`NonObjectTypeTrait`, `MaybeObjectTypeTrait`, `MixedType`, `StrictMixedType`): `ErrorType`. - `ObjectType` and `ObjectTypeTrait` users (and `ClosureType` via its inner `ObjectType`): if the class is known and `isFinalByKeyword()`, return the literal class name (`ConstantStringType($name, true)`); otherwise `IntersectionType[ClassString<X>, AccessoryLiteralStringType]`. - `EnumCaseObjectType`: explicitly skips the finality collapse and always returns `class-string<EnumName>&literal-string` — even though PHP enums report as `final`, the case-binding shape is what call sites expect. - `StaticType`: uses its own `getClassStringType()` so static binding is preserved (`class-string<static>` / `class-string<$this>`). - `NonexistentParentClassType`: `class-string&literal-string` (matches the original "isObject->yes, no class names" branch). - All template variants (via `TemplateTypeTrait`): `class-string<T>& literal-string`, with the same final-class collapse when the bound class is final. Required because templates with non-object bounds (e.g. `T of mixed`) fall through `MaybeObjectTypeTrait`'s `ErrorType` default and would otherwise lose the `class-string<T>` shape. - `NeverType`: pass through. - `UnionType`/`IntersectionType`: distribute, threading the `ReflectionProvider` through. - `LateResolvableTypeTrait`: delegates to `resolve()`. The `ReflectionProvider` is passed as a method argument because the final-class collapse can only be decided at the `ReflectionProvider` level (matches the `Type::getCallableParametersAcceptors($scope)` precedent for dependency-carrying Type methods). Pure refactor: full test suite + phpstan + cs pass.
1 parent c2317bb commit 8fafec7

18 files changed

Lines changed: 169 additions & 49 deletions

src/Reflection/InitializerExprTypeResolver.php

Lines changed: 1 addition & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@
9393
use PHPStan\Type\TypeCombinator;
9494
use PHPStan\Type\TypehintHelper;
9595
use PHPStan\Type\TypeResult;
96-
use PHPStan\Type\TypeTraverser;
9796
use PHPStan\Type\TypeUtils;
9897
use PHPStan\Type\TypeWithClassName;
9998
use PHPStan\Type\UnionType;
@@ -2451,54 +2450,7 @@ public function getClassConstFetchTypeByReflection(Name|Expr $class, string $con
24512450
}
24522451

24532452
if (strtolower($constantName) === 'class') {
2454-
return TypeTraverser::map(
2455-
$constantClassType,
2456-
function (Type $type, callable $traverse): Type {
2457-
if ($type instanceof UnionType || $type instanceof IntersectionType) {
2458-
return $traverse($type);
2459-
}
2460-
2461-
if ($type instanceof NullType) {
2462-
return $type;
2463-
}
2464-
2465-
if ($type instanceof EnumCaseObjectType) {
2466-
return new IntersectionType([
2467-
new GenericClassStringType(new ObjectType($type->getClassName())),
2468-
new AccessoryLiteralStringType(),
2469-
]);
2470-
}
2471-
2472-
$objectClassNames = $type->getObjectClassNames();
2473-
if (count($objectClassNames) > 1) {
2474-
throw new ShouldNotHappenException();
2475-
}
2476-
2477-
if ($type instanceof TemplateType && $objectClassNames === []) {
2478-
return new IntersectionType([
2479-
new GenericClassStringType($type),
2480-
new AccessoryLiteralStringType(),
2481-
]);
2482-
} elseif ($objectClassNames !== [] && $this->getReflectionProvider()->hasClass($objectClassNames[0])) {
2483-
$reflection = $this->getReflectionProvider()->getClass($objectClassNames[0]);
2484-
if ($reflection->isFinalByKeyword()) {
2485-
return new ConstantStringType($reflection->getName(), true);
2486-
}
2487-
2488-
return new IntersectionType([
2489-
new GenericClassStringType($type),
2490-
new AccessoryLiteralStringType(),
2491-
]);
2492-
} elseif ($type->isObject()->yes()) {
2493-
return new IntersectionType([
2494-
new ClassStringType(),
2495-
new AccessoryLiteralStringType(),
2496-
]);
2497-
}
2498-
2499-
return new ErrorType();
2500-
},
2501-
);
2453+
return $constantClassType->toClassConstantType($this->getReflectionProvider());
25022454
}
25032455

25042456
if ($constantClassType->isClassString()->yes()) {

src/Type/ClosureType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
use PHPStan\Reflection\PassedByReference;
3131
use PHPStan\Reflection\Php\ClosureCallUnresolvedMethodPrototypeReflection;
3232
use PHPStan\Reflection\Php\DummyParameter;
33+
use PHPStan\Reflection\ReflectionProvider;
3334
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
3435
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
3536
use PHPStan\TrinaryLogic;
@@ -520,6 +521,11 @@ public function toGetClassResultType(): Type
520521
return $this->getClassStringType();
521522
}
522523

524+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
525+
{
526+
return $this->objectType->toClassConstantType($reflectionProvider);
527+
}
528+
523529
public function toAbsoluteNumber(): Type
524530
{
525531
return new ErrorType();

src/Type/Enum/EnumCaseObjectType.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
use PHPStan\Reflection\ExtendedPropertyReflection;
1212
use PHPStan\Reflection\Php\EnumPropertyReflection;
1313
use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection;
14+
use PHPStan\Reflection\ReflectionProvider;
1415
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
1516
use PHPStan\ShouldNotHappenException;
1617
use PHPStan\TrinaryLogic;
1718
use PHPStan\Type\AcceptsResult;
19+
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
1820
use PHPStan\Type\CompoundType;
1921
use PHPStan\Type\Constant\ConstantStringType;
2022
use PHPStan\Type\GeneralizePrecision;
2123
use PHPStan\Type\Generic\GenericClassStringType;
24+
use PHPStan\Type\IntersectionType;
2225
use PHPStan\Type\IsSuperTypeOfResult;
2326
use PHPStan\Type\NeverType;
2427
use PHPStan\Type\ObjectType;
@@ -228,6 +231,17 @@ public function getClassStringType(): Type
228231
return new GenericClassStringType(new ObjectType($this->getClassName()));
229232
}
230233

234+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
235+
{
236+
// Enum cases always read their `::class` as the bare enum class
237+
// name. Skip the parent's finality collapse: even though enum
238+
// classes are reported as `final` by reflection, `Foo::Bar::class`
239+
// in user code should resolve to `class-string<Foo>&literal-string`,
240+
// not the literal `'Foo'`, to keep the case-binding visible at
241+
// downstream sites.
242+
return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]);
243+
}
244+
231245
public function toPhpDocNode(): TypeNode
232246
{
233247
return new ConstTypeNode(

src/Type/Generic/TemplateTypeTrait.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44

55
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
66
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
7+
use PHPStan\Reflection\ReflectionProvider;
78
use PHPStan\TrinaryLogic;
89
use PHPStan\Type\AcceptsResult;
10+
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
11+
use PHPStan\Type\Constant\ConstantStringType;
912
use PHPStan\Type\ErrorType;
1013
use PHPStan\Type\IntersectionType;
1114
use PHPStan\Type\IsSuperTypeOfResult;
1215
use PHPStan\Type\MixedType;
1316
use PHPStan\Type\NeverType;
17+
use PHPStan\Type\NullType;
1418
use PHPStan\Type\RecursionGuard;
1519
use PHPStan\Type\SubtractableType;
1620
use PHPStan\Type\Type;
@@ -264,6 +268,32 @@ public function toCoercedArgumentType(bool $strictTypes): Type
264268
return $this;
265269
}
266270

271+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
272+
{
273+
// `T::class` keeps the template variable visible — express the
274+
// result as `class-string<T>&literal-string` rather than the bound
275+
// class, regardless of whether `T` has an object bound or not.
276+
// Only when the bound is a known final class does the result
277+
// collapse to the literal class name (the bound is the only
278+
// possible substitution in that case).
279+
if ($this->isNull()->yes()) {
280+
return new NullType();
281+
}
282+
283+
$classNames = $this->getObjectClassNames();
284+
if (count($classNames) === 1 && $reflectionProvider->hasClass($classNames[0])) {
285+
$reflection = $reflectionProvider->getClass($classNames[0]);
286+
if ($reflection->isFinalByKeyword()) {
287+
return new ConstantStringType($reflection->getName(), true);
288+
}
289+
}
290+
291+
return new IntersectionType([
292+
new GenericClassStringType($this),
293+
new AccessoryLiteralStringType(),
294+
]);
295+
}
296+
267297
public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
268298
{
269299
if (

src/Type/IntersectionType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPStan\Reflection\MissingMethodFromReflectionException;
2020
use PHPStan\Reflection\MissingPropertyFromReflectionException;
2121
use PHPStan\Reflection\ParametersAcceptorSelector;
22+
use PHPStan\Reflection\ReflectionProvider;
2223
use PHPStan\Reflection\TrivialParametersAcceptor;
2324
use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
2425
use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
@@ -1421,6 +1422,11 @@ public function toGetClassResultType(): Type
14211422
return $this->intersectTypes(static fn (Type $type): Type => $type->toGetClassResultType());
14221423
}
14231424

1425+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1426+
{
1427+
return $this->intersectTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1428+
}
1429+
14241430
public function toAbsoluteNumber(): Type
14251431
{
14261432
$type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());

src/Type/MixedType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Reflection\Dummy\DummyPropertyReflection;
1414
use PHPStan\Reflection\ExtendedMethodReflection;
1515
use PHPStan\Reflection\ExtendedPropertyReflection;
16+
use PHPStan\Reflection\ReflectionProvider;
1617
use PHPStan\Reflection\TrivialParametersAcceptor;
1718
use PHPStan\Reflection\Type\CallbackUnresolvedMethodPrototypeReflection;
1819
use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection;
@@ -634,6 +635,13 @@ public function toGetClassResultType(): Type
634635
return new UnionType([$classString, new ConstantBooleanType(false)]);
635636
}
636637

638+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
639+
{
640+
// `mixed::class` is undefined — the original `TypeTraverser` cb fell
641+
// through to `ErrorType` for any leaf that wasn't a definite object.
642+
return new ErrorType();
643+
}
644+
637645
public function toAbsoluteNumber(): Type
638646
{
639647
return $this->toNumber()->toAbsoluteNumber();

src/Type/NeverType.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Reflection\ClassMemberAccessAnswerer;
1010
use PHPStan\Reflection\ExtendedMethodReflection;
1111
use PHPStan\Reflection\ExtendedPropertyReflection;
12+
use PHPStan\Reflection\ReflectionProvider;
1213
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
1314
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
1415
use PHPStan\ShouldNotHappenException;
@@ -444,6 +445,11 @@ public function toGetClassResultType(): Type
444445
return $this;
445446
}
446447

448+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
449+
{
450+
return $this;
451+
}
452+
447453
public function toAbsoluteNumber(): Type
448454
{
449455
return $this;

src/Type/NonexistentParentClassType.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
use PHPStan\Reflection\ClassMemberAccessAnswerer;
1010
use PHPStan\Reflection\ExtendedMethodReflection;
1111
use PHPStan\Reflection\ExtendedPropertyReflection;
12+
use PHPStan\Reflection\ReflectionProvider;
1213
use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
1314
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
1415
use PHPStan\ShouldNotHappenException;
1516
use PHPStan\TrinaryLogic;
17+
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
1618
use PHPStan\Type\Enum\EnumCaseObjectType;
1719
use PHPStan\Type\Traits\NonArrayTypeTrait;
1820
use PHPStan\Type\Traits\NonCallableTypeTrait;
@@ -173,6 +175,11 @@ public function toGetClassResultType(): Type
173175
return $this->getClassStringType();
174176
}
175177

178+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
179+
{
180+
return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]);
181+
}
182+
176183
public function toAbsoluteNumber(): Type
177184
{
178185
return new ErrorType();

src/Type/NullType.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Php\PhpVersion;
66
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
77
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
8+
use PHPStan\Reflection\ReflectionProvider;
89
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\Constant\ConstantArrayType;
1011
use PHPStan\Type\Constant\ConstantBooleanType;
@@ -149,6 +150,13 @@ public function toBitwiseNotType(): Type
149150
return new ErrorType();
150151
}
151152

153+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
154+
{
155+
// Null `::class` reads as `null` (mirrors how `null::class` flows
156+
// through the `::class` resolution pipeline alongside object types).
157+
return $this;
158+
}
159+
152160
public function toAbsoluteNumber(): Type
153161
{
154162
return $this->toNumber()->toAbsoluteNumber();

src/Type/ObjectType.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\Reflection\ExtendedMethodReflection;
2424
use PHPStan\Reflection\ExtendedPropertyReflection;
2525
use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension;
26+
use PHPStan\Reflection\ReflectionProvider;
2627
use PHPStan\Reflection\ReflectionProviderStaticAccessor;
2728
use PHPStan\Reflection\TrivialParametersAcceptor;
2829
use PHPStan\Reflection\Type\CallbackUnresolvedPropertyPrototypeReflection;
@@ -33,6 +34,7 @@
3334
use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
3435
use PHPStan\ShouldNotHappenException;
3536
use PHPStan\TrinaryLogic;
37+
use PHPStan\Type\Accessory\AccessoryLiteralStringType;
3638
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
3739
use PHPStan\Type\Accessory\AccessoryNumericStringType;
3840
use PHPStan\Type\Accessory\HasOffsetValueType;
@@ -773,6 +775,18 @@ public function toGetClassResultType(): Type
773775
return $this->getClassStringType();
774776
}
775777

778+
public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
779+
{
780+
if ($reflectionProvider->hasClass($this->className)) {
781+
$reflection = $reflectionProvider->getClass($this->className);
782+
if ($reflection->isFinalByKeyword()) {
783+
return new ConstantStringType($reflection->getName(), true);
784+
}
785+
}
786+
787+
return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]);
788+
}
789+
776790
public function toAbsoluteNumber(): Type
777791
{
778792
return $this->toNumber()->toAbsoluteNumber();

0 commit comments

Comments
 (0)