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