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