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