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