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