1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use LogicException;
8: use PhpParser\Node;
9: use PhpParser\Node\Stmt\EnumCase;
10: use ReflectionClass as CoreReflectionClass;
11: use PHPStan\BetterReflection\NodeCompiler\CompiledValue;
12: use PHPStan\BetterReflection\NodeCompiler\CompileNodeToValue;
13: use PHPStan\BetterReflection\NodeCompiler\CompilerContext;
14: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
15: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
16: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionEnumCaseStringCast;
17: use PHPStan\BetterReflection\Reflector\Reflector;
18: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
19: use PHPStan\BetterReflection\Util\GetLastDocComment;
20:
21: use function assert;
22: use function is_array;
23: use function is_int;
24: use function is_string;
25:
26: /** @psalm-immutable */
27: class ReflectionEnumCase
28: {
29: private Reflector $reflector;
30: private ReflectionEnum $enum;
31: /** @var non-empty-string */
32: private string $name;
33:
34: /**
35: * The value expression, its exported cache form (parsed into an Expr only when asked
36: * for), or null.
37: *
38: * @var Node\Expr|array<string, mixed>|null
39: */
40: private $value;
41:
42: /** @var list<ReflectionAttribute> */
43: private array $attributes;
44:
45: /** @var non-empty-string|null */
46: private $docComment;
47:
48: /** @var positive-int */
49: private int $startLine;
50:
51: /** @var positive-int */
52: private int $endLine;
53:
54: /** @var positive-int */
55: private int $startColumn;
56:
57: /** @var positive-int */
58: private int $endColumn;
59:
60: /** @psalm-allow-private-mutation
61: * @var \PHPStan\BetterReflection\NodeCompiler\CompiledValue|null */
62: private $compiledValue = null;
63:
64: private function __construct(
65: Reflector $reflector,
66: EnumCase $node,
67: ReflectionEnum $enum
68: ) {
69: $this->reflector = $reflector;
70: $this->enum = $enum;
71: $this->name = $node->name->toString();
72:
73: $this->value = $node->expr;
74: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
75: $this->docComment = GetLastDocComment::forNode($node);
76:
77: $startLine = $node->getStartLine();
78: assert($startLine > 0);
79: $endLine = $node->getEndLine();
80: assert($endLine > 0);
81:
82: $this->startLine = $startLine;
83: $this->endLine = $endLine;
84: $this->startColumn = CalculateReflectionColumn::getStartColumn($this->enum->getLocatedSource()->getSource(), $node);
85: $this->endColumn = CalculateReflectionColumn::getEndColumn($this->enum->getLocatedSource()->getSource(), $node);
86: }
87:
88: /**
89: * @return array<string, mixed>
90: */
91: public function exportToCache(): array
92: {
93: return [
94: 'name' => $this->name,
95: 'value' => $this->value === null || is_array($this->value) ? $this->value : ExprCacheHelper::export($this->value),
96: 'attributes' => array_map(
97: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
98: $this->attributes,
99: ),
100: 'docComment' => $this->docComment,
101: 'startLine' => $this->startLine,
102: 'endLine' => $this->endLine,
103: 'startColumn' => $this->startColumn,
104: 'endColumn' => $this->endColumn,
105: ];
106: }
107:
108: /**
109: * @param array<string, mixed> $data
110: */
111: public static function importFromCache(Reflector $reflector, array $data, ReflectionEnum $enum): self
112: {
113: $reflection = new CoreReflectionClass(self::class);
114: /** @var self $ref */
115: $ref = $reflection->newInstanceWithoutConstructor();
116: $ref->reflector = $reflector;
117: $ref->enum = $enum;
118: $ref->name = $data['name'];
119:
120: $ref->value = $data['value'];
121:
122: $ref->attributes = array_map(
123: static fn ($attrData) => ReflectionAttribute::importFromCache($reflector, $attrData, $ref),
124: $data['attributes'],
125: );
126: $ref->docComment = $data['docComment'];
127: $ref->startLine = $data['startLine'];
128: $ref->endLine = $data['endLine'];
129: $ref->startColumn = $data['startColumn'];
130: $ref->endColumn = $data['endColumn'];
131:
132: return $ref;
133: }
134:
135: /** @internal */
136: public static function createFromNode(
137: Reflector $reflector,
138: EnumCase $node,
139: ReflectionEnum $enum
140: ): self {
141: return new self($reflector, $node, $enum);
142: }
143:
144: /** @return non-empty-string */
145: public function getName(): string
146: {
147: return $this->name;
148: }
149:
150: /**
151: * While ReflectionEnum::isBacked() is a sufficient check when working with valid PHP code,
152: * with an invalid enum declaration we can still encounter a back enum case with a missing value.
153: */
154: public function hasValueExpression(): bool
155: {
156: return $this->value !== null;
157: }
158:
159: /**
160: * Check self::hasValueExpression() being true first to avoid throwing exception.
161: *
162: * @throws LogicException
163: */
164: public function getValueExpression(): Node\Expr
165: {
166: if ($this->value === null) {
167: throw new LogicException('This enum case does not have a value');
168: }
169:
170: if (is_array($this->value)) {
171: $this->value = ExprCacheHelper::import($this->value);
172: }
173:
174: return $this->value;
175: }
176:
177: /**
178: * @return int|string
179: */
180: public function getValue()
181: {
182: $value = $this->getCompiledValue()->value;
183: assert(is_string($value) || is_int($value));
184:
185: return $value;
186: }
187:
188: /**
189: * Check self::hasValueExpression() being true first to avoid throwing exception.
190: *
191: * @throws LogicException
192: */
193: private function getCompiledValue(): CompiledValue
194: {
195: if ($this->value === null) {
196: throw new LogicException('This enum case does not have a value');
197: }
198:
199: if ($this->compiledValue === null) {
200: $this->compiledValue = (new CompileNodeToValue())->__invoke(
201: $this->getValueExpression(),
202: new CompilerContext($this->reflector, $this),
203: );
204: }
205:
206: return $this->compiledValue;
207: }
208:
209: /** @return positive-int */
210: public function getStartLine(): int
211: {
212: return $this->startLine;
213: }
214:
215: /** @return positive-int */
216: public function getEndLine(): int
217: {
218: return $this->endLine;
219: }
220:
221: /** @return positive-int */
222: public function getStartColumn(): int
223: {
224: return $this->startColumn;
225: }
226:
227: /** @return positive-int */
228: public function getEndColumn(): int
229: {
230: return $this->endColumn;
231: }
232:
233: public function getDeclaringEnum(): ReflectionEnum
234: {
235: return $this->enum;
236: }
237:
238: public function getDeclaringClass(): ReflectionClass
239: {
240: return $this->enum;
241: }
242:
243: /** @return non-empty-string|null */
244: public function getDocComment(): ?string
245: {
246: return $this->docComment;
247: }
248:
249: public function isDeprecated(): bool
250: {
251: return DeprecatedHelper::isDeprecated($this);
252: }
253:
254: /** @return list<ReflectionAttribute> */
255: public function getAttributes(): array
256: {
257: return $this->attributes;
258: }
259:
260: /** @return list<ReflectionAttribute> */
261: public function getAttributesByName(string $name): array
262: {
263: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
264: }
265:
266: /**
267: * @param class-string $className
268: *
269: * @return list<ReflectionAttribute>
270: */
271: public function getAttributesByInstance(string $className): array
272: {
273: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
274: }
275:
276: /** @return non-empty-string */
277: public function __toString(): string
278: {
279: return ReflectionEnumCaseStringCast::toString($this);
280: }
281: }
282: