1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type\Enum;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
7: use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
9: use PHPStan\Reflection\ClassMemberAccessAnswerer;
10: use PHPStan\Reflection\ClassReflection;
11: use PHPStan\Reflection\ExtendedPropertyReflection;
12: use PHPStan\Reflection\Php\EnumPropertyReflection;
13: use PHPStan\Reflection\Php\EnumUnresolvedPropertyPrototypeReflection;
14: use PHPStan\Reflection\ReflectionProvider;
15: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
16: use PHPStan\ShouldNotHappenException;
17: use PHPStan\TrinaryLogic;
18: use PHPStan\Type\AcceptsResult;
19: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
20: use PHPStan\Type\CompoundType;
21: use PHPStan\Type\Constant\ConstantStringType;
22: use PHPStan\Type\GeneralizePrecision;
23: use PHPStan\Type\Generic\GenericClassStringType;
24: use PHPStan\Type\InstanceofDeprecated;
25: use PHPStan\Type\IntersectionType;
26: use PHPStan\Type\IsSuperTypeOfResult;
27: use PHPStan\Type\NeverType;
28: use PHPStan\Type\ObjectType;
29: use PHPStan\Type\SubtractableType;
30: use PHPStan\Type\Type;
31: use PHPStan\Type\VerbosityLevel;
32: use function sprintf;
33:
34: /** @api */
35: #[InstanceofDeprecated(insteadUse: 'Type::getEnumCaseObject() or Type::getEnumCases()')]
36: class EnumCaseObjectType extends ObjectType
37: {
38:
39: /** @api */
40: public function __construct(
41: string $className,
42: private readonly string $enumCaseName,
43: ?ClassReflection $classReflection = null,
44: )
45: {
46: parent::__construct($className, classReflection: $classReflection);
47: }
48:
49: public function getEnumCaseName(): string
50: {
51: return $this->enumCaseName;
52: }
53:
54: public function describe(VerbosityLevel $level): string
55: {
56: $parent = parent::describe($level);
57:
58: return sprintf('%s::%s', $parent, $this->enumCaseName);
59: }
60:
61: public function equals(Type $type): bool
62: {
63: if (!$type instanceof self) {
64: return false;
65: }
66:
67: return $this->enumCaseName === $type->enumCaseName &&
68: $this->getClassName() === $type->getClassName();
69: }
70:
71: public function accepts(Type $type, bool $strictTypes): AcceptsResult
72: {
73: return $this->isSuperTypeOf($type)->toAcceptsResult();
74: }
75:
76: public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
77: {
78: if ($type instanceof self) {
79: return IsSuperTypeOfResult::createFromBoolean(
80: $this->enumCaseName === $type->enumCaseName && $this->getClassName() === $type->getClassName(),
81: );
82: }
83:
84: if ($type instanceof CompoundType) {
85: return $type->isSubTypeOf($this);
86: }
87:
88: if (
89: $type instanceof SubtractableType
90: && $type->getSubtractedType() !== null
91: ) {
92: $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this);
93: if ($isSuperType->yes()) {
94: return IsSuperTypeOfResult::createNo();
95: }
96: }
97:
98: $parent = new parent($this->getClassName(), $this->getSubtractedType(), $this->getClassReflection());
99:
100: return $parent->isSuperTypeOf($type)->and(IsSuperTypeOfResult::createMaybe());
101: }
102:
103: public function subtract(Type $type): Type
104: {
105: return $this->changeSubtractedType($type);
106: }
107:
108: public function getTypeWithoutSubtractedType(): Type
109: {
110: return $this;
111: }
112:
113: public function changeSubtractedType(?Type $subtractedType): Type
114: {
115: if ($subtractedType === null || ! $this->equals($subtractedType)) {
116: return $this;
117: }
118:
119: return new NeverType();
120: }
121:
122: public function getSubtractedType(): ?Type
123: {
124: return null;
125: }
126:
127: public function tryRemove(Type $typeToRemove): ?Type
128: {
129: if ($this->isSuperTypeOf($typeToRemove)->yes()) {
130: return $this->subtract($typeToRemove);
131: }
132:
133: return null;
134: }
135:
136: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
137: {
138: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope);
139: }
140:
141: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
142: {
143: $classReflection = $this->getClassReflection();
144: if ($classReflection === null) {
145: return parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope);
146:
147: }
148: if ($propertyName === 'name') {
149: return new EnumUnresolvedPropertyPrototypeReflection(
150: new EnumPropertyReflection($propertyName, $classReflection, new ConstantStringType($this->enumCaseName)),
151: );
152: }
153:
154: if ($classReflection->isBackedEnum() && $propertyName === 'value') {
155: if ($classReflection->hasEnumCase($this->enumCaseName)) {
156: $enumCase = $classReflection->getEnumCase($this->enumCaseName);
157: $valueType = $enumCase->getBackingValueType();
158: if ($valueType === null) {
159: throw new ShouldNotHappenException();
160: }
161:
162: return new EnumUnresolvedPropertyPrototypeReflection(
163: new EnumPropertyReflection($propertyName, $classReflection, $valueType),
164: );
165: }
166: }
167:
168: return parent::getUnresolvedInstancePropertyPrototype($propertyName, $scope);
169: }
170:
171: public function hasStaticProperty(string $propertyName): TrinaryLogic
172: {
173: return TrinaryLogic::createNo();
174: }
175:
176: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
177: {
178: throw new ShouldNotHappenException();
179: }
180:
181: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
182: {
183: throw new ShouldNotHappenException();
184: }
185:
186: public function getBackingValueType(): ?Type
187: {
188: $classReflection = $this->getClassReflection();
189: if ($classReflection === null) {
190: return null;
191: }
192:
193: if (!$classReflection->isBackedEnum()) {
194: return null;
195: }
196:
197: if ($classReflection->hasEnumCase($this->enumCaseName)) {
198: $enumCase = $classReflection->getEnumCase($this->enumCaseName);
199:
200: return $enumCase->getBackingValueType();
201: }
202:
203: return null;
204: }
205:
206: public function generalize(GeneralizePrecision $precision): Type
207: {
208: return new parent($this->getClassName(), null, $this->getClassReflection());
209: }
210:
211: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
212: {
213: return TrinaryLogic::createNo();
214: }
215:
216: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
217: {
218: return TrinaryLogic::createNo();
219: }
220:
221: public function getEnumCases(): array
222: {
223: return [$this];
224: }
225:
226: public function getEnumCaseObject(): ?EnumCaseObjectType
227: {
228: return $this;
229: }
230:
231: public function getClassStringType(): Type
232: {
233: return new GenericClassStringType(new ObjectType($this->getClassName()));
234: }
235:
236: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
237: {
238: // Enum cases always read their `::class` as the bare enum class
239: // name. Skip the parent's finality collapse: even though enum
240: // classes are reported as `final` by reflection, `Foo::Bar::class`
241: // in user code should resolve to `class-string<Foo>&literal-string`,
242: // not the literal `'Foo'`, to keep the case-binding visible at
243: // downstream sites.
244: return new IntersectionType([$this->getClassStringType(), new AccessoryLiteralStringType()]);
245: }
246:
247: public function toPhpDocNode(): TypeNode
248: {
249: return new ConstTypeNode(
250: new ConstFetchNode(
251: $this->getClassName(),
252: $this->getEnumCaseName(),
253: ),
254: );
255: }
256:
257: }
258: