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(
61: Reflector $reflector,
62: EnumCase $node,
63: ReflectionEnum $enum
64: ) {
65: $this->reflector = $reflector;
66: $this->enum = $enum;
67: $this->name = $node->name->toString();
68:
69: $this->value = $node->expr;
70: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
71: $this->docComment = GetLastDocComment::forNode($node);
72:
73: $startLine = $node->getStartLine();
74: assert($startLine > 0);
75: $endLine = $node->getEndLine();
76: assert($endLine > 0);
77:
78: $this->startLine = $startLine;
79: $this->endLine = $endLine;
80: $this->startColumn = CalculateReflectionColumn::getStartColumn($this->enum->getLocatedSource()->getSource(), $node);
81: $this->endColumn = CalculateReflectionColumn::getEndColumn($this->enum->getLocatedSource()->getSource(), $node);
82: }
83:
84: /**
85: * @return array<string, mixed>
86: */
87: public function exportToCache(): array
88: {
89: return [
90: 'name' => $this->name,
91: 'value' => $this->value !== null ? ExprCacheHelper::export($this->value) : null,
92: 'attributes' => array_map(
93: static fn (ReflectionAttribute $attr) => $attr->exportToCache(),
94: $this->attributes,
95: ),
96: 'docComment' => $this->docComment,
97: 'startLine' => $this->startLine,
98: 'endLine' => $this->endLine,
99: 'startColumn' => $this->startColumn,
100: 'endColumn' => $this->endColumn,
101: ];
102: }
103:
104: /**
105: * @param array<string, mixed> $data
106: */
107: public static function importFromCache(Reflector $reflector, array $data, ReflectionEnum $enum): self
108: {
109: $reflection = new CoreReflectionClass(self::class);
110: /** @var self $ref */
111: $ref = $reflection->newInstanceWithoutConstructor();
112: $ref->reflector = $reflector;
113: $ref->enum = $enum;
114: $ref->name = $data['name'];
115:
116: if ($data['value'] !== null) {
117: $ref->value = ExprCacheHelper::import($data['value']);
118: } else {
119: $ref->value = null;
120: }
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: return $this->value;
171: }
172:
173: /**
174: * @return int|string
175: */
176: public function getValue()
177: {
178: $value = $this->getCompiledValue()->value;
179: assert(is_string($value) || is_int($value));
180:
181: return $value;
182: }
183:
184: /**
185: * Check self::hasValueExpression() being true first to avoid throwing exception.
186: *
187: * @throws LogicException
188: */
189: private function getCompiledValue(): CompiledValue
190: {
191: if ($this->value === null) {
192: throw new LogicException('This enum case does not have a value');
193: }
194:
195: if ($this->compiledValue === null) {
196: $this->compiledValue = (new CompileNodeToValue())->__invoke(
197: $this->value,
198: new CompilerContext($this->reflector, $this),
199: );
200: }
201:
202: return $this->compiledValue;
203: }
204:
205: /** @return positive-int */
206: public function getStartLine(): int
207: {
208: return $this->startLine;
209: }
210:
211: /** @return positive-int */
212: public function getEndLine(): int
213: {
214: return $this->endLine;
215: }
216:
217: /** @return positive-int */
218: public function getStartColumn(): int
219: {
220: return $this->startColumn;
221: }
222:
223: /** @return positive-int */
224: public function getEndColumn(): int
225: {
226: return $this->endColumn;
227: }
228:
229: public function getDeclaringEnum(): ReflectionEnum
230: {
231: return $this->enum;
232: }
233:
234: public function getDeclaringClass(): ReflectionClass
235: {
236: return $this->enum;
237: }
238:
239: /** @return non-empty-string|null */
240: public function getDocComment(): ?string
241: {
242: return $this->docComment;
243: }
244:
245: public function isDeprecated(): bool
246: {
247: return DeprecatedHelper::isDeprecated($this);
248: }
249:
250: /** @return list<ReflectionAttribute> */
251: public function getAttributes(): array
252: {
253: return $this->attributes;
254: }
255:
256: /** @return list<ReflectionAttribute> */
257: public function getAttributesByName(string $name): array
258: {
259: return ReflectionAttributeHelper::filterAttributesByName($this->getAttributes(), $name);
260: }
261:
262: /**
263: * @param class-string $className
264: *
265: * @return list<ReflectionAttribute>
266: */
267: public function getAttributesByInstance(string $className): array
268: {
269: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
270: }
271:
272: /** @return non-empty-string */
273: public function __toString(): string
274: {
275: return ReflectionEnumCaseStringCast::toString($this);
276: }
277: }
278: