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