1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection\Adapter;
6:
7: use OutOfBoundsException;
8: use ReflectionClass as CoreReflectionClass;
9: use ReflectionEnum as CoreReflectionEnum;
10: use ReflectionException as CoreReflectionException;
11: use ReflectionExtension as CoreReflectionExtension;
12: use ReflectionMethod as CoreReflectionMethod;
13: use PHPStan\BetterReflection\Reflection\ReflectionAttribute as BetterReflectionAttribute;
14: use PHPStan\BetterReflection\Reflection\ReflectionClass as BetterReflectionClass;
15: use PHPStan\BetterReflection\Reflection\ReflectionClassConstant as BetterReflectionClassConstant;
16: use PHPStan\BetterReflection\Reflection\ReflectionEnum as BetterReflectionEnum;
17: use PHPStan\BetterReflection\Reflection\ReflectionEnumCase as BetterReflectionEnumCase;
18: use PHPStan\BetterReflection\Reflection\ReflectionMethod as BetterReflectionMethod;
19: use PHPStan\BetterReflection\Reflection\ReflectionProperty as BetterReflectionProperty;
20: use PHPStan\BetterReflection\Util\FileHelper;
21: use PHPStan\BetterReflection\Util\Memoize;
22: use UnitEnum;
23: use ValueError;
24:
25: use function array_combine;
26: use function array_map;
27: use function array_values;
28: use function constant;
29: use function sprintf;
30: use function strtolower;
31:
32: /**
33: * @psalm-suppress PropertyNotSetInConstructor
34: * @psalm-immutable
35: */
36: final class ReflectionEnum extends CoreReflectionEnum
37: {
38: /** @var Memoize<list<ReflectionEnumUnitCase>|list<ReflectionEnumBackedCase>> */
39: private Memoize $cases;
40:
41: public function __construct(private BetterReflectionEnum $betterReflectionEnum)
42: {
43: /** @phpstan-ignore unset.readOnlyPropertyByPhpDoc */
44: unset($this->name);
45:
46: $enum = $this->betterReflectionEnum;
47:
48: /** @phpstan-ignore argument.type */
49: $this->cases = new Memoize(static function () use ($enum) {
50: $mappedCases = [];
51:
52: foreach ($enum->getCases() as $case) {
53: if ($enum->isBacked()) {
54: $mappedCases[] = new ReflectionEnumBackedCase($case);
55: } else {
56: $mappedCases[] = new ReflectionEnumUnitCase($case);
57: }
58: }
59:
60: return $mappedCases;
61: });
62: }
63:
64: /** @return non-empty-string */
65: public function __toString(): string
66: {
67: return $this->betterReflectionEnum->__toString();
68: }
69:
70: public function __get(string $name): mixed
71: {
72: if ($name === 'name') {
73: return $this->betterReflectionEnum->getName();
74: }
75:
76: throw new OutOfBoundsException(sprintf('Property %s::$%s does not exist.', self::class, $name));
77: }
78:
79: /** @return class-string */
80: public function getName(): string
81: {
82: /** @phpstan-var class-string<UnitEnum> */
83: return $this->betterReflectionEnum->getName();
84: }
85:
86: public function getBetterReflection(): BetterReflectionEnum
87: {
88: return $this->betterReflectionEnum;
89: }
90:
91: public function isAnonymous(): bool
92: {
93: return $this->betterReflectionEnum->isAnonymous();
94: }
95:
96: public function isInternal(): bool
97: {
98: return $this->betterReflectionEnum->isInternal();
99: }
100:
101: public function isUserDefined(): bool
102: {
103: return $this->betterReflectionEnum->isUserDefined();
104: }
105:
106: public function isInstantiable(): bool
107: {
108: return $this->betterReflectionEnum->isInstantiable();
109: }
110:
111: public function isCloneable(): bool
112: {
113: return $this->betterReflectionEnum->isCloneable();
114: }
115:
116: /** @return non-empty-string|false */
117: public function getFileName(): string|false
118: {
119: $fileName = $this->betterReflectionEnum->getFileName();
120:
121: return $fileName !== null ? FileHelper::normalizeSystemPath($fileName) : false;
122: }
123:
124: public function getStartLine(): int
125: {
126: return $this->betterReflectionEnum->getStartLine();
127: }
128:
129: public function getEndLine(): int
130: {
131: return $this->betterReflectionEnum->getEndLine();
132: }
133:
134: public function getDocComment(): string|false
135: {
136: return $this->betterReflectionEnum->getDocComment() ?? false;
137: }
138:
139: /** @return ReflectionMethod|null */
140: public function getConstructor(): ?CoreReflectionMethod
141: {
142: $constructor = $this->betterReflectionEnum->getConstructor();
143:
144: if ($constructor === null) {
145: return null;
146: }
147:
148: return new ReflectionMethod($constructor);
149: }
150:
151: public function hasMethod(string $name): bool
152: {
153: if ($name === '') {
154: return false;
155: }
156:
157: return $this->betterReflectionEnum->hasMethod($name);
158: }
159:
160: public function getMethod(string $name): ReflectionMethod
161: {
162: $method = $name !== '' ? $this->betterReflectionEnum->getMethod($name) : null;
163:
164: if ($method === null) {
165: throw new CoreReflectionException(sprintf('Method %s::%s() does not exist', $this->betterReflectionEnum->getName(), $name));
166: }
167:
168: return new ReflectionMethod($method);
169: }
170:
171: /**
172: * @param int-mask-of<ReflectionMethod::IS_*>|null $filter
173: *
174: * @return list<ReflectionMethod>
175: */
176: public function getMethods(int|null $filter = null): array
177: {
178: /** @psalm-suppress ImpureFunctionCall */
179: return array_values(array_map(
180: static fn (BetterReflectionMethod $method): ReflectionMethod => new ReflectionMethod($method),
181: $this->betterReflectionEnum->getMethods($filter ?? 0),
182: ));
183: }
184:
185: public function hasProperty(string $name): bool
186: {
187: if ($name === '') {
188: return false;
189: }
190:
191: return $this->betterReflectionEnum->hasProperty($name);
192: }
193:
194: public function getProperty(string $name): ReflectionProperty
195: {
196: $betterReflectionProperty = $name !== '' ? $this->betterReflectionEnum->getProperty($name) : null;
197:
198: if ($betterReflectionProperty === null) {
199: throw new CoreReflectionException(sprintf('Property %s::$%s does not exist', $this->betterReflectionEnum->getName(), $name));
200: }
201:
202: return new ReflectionProperty($betterReflectionProperty);
203: }
204:
205: /**
206: * @param int-mask-of<ReflectionProperty::IS_*>|null $filter
207: *
208: * @return list<ReflectionProperty>
209: */
210: public function getProperties(int|null $filter = null): array
211: {
212: /** @psalm-suppress ImpureFunctionCall */
213: return array_values(array_map(
214: static fn (BetterReflectionProperty $property): ReflectionProperty => new ReflectionProperty($property),
215: $this->betterReflectionEnum->getProperties($filter ?? 0),
216: ));
217: }
218:
219: public function hasConstant(string $name): bool
220: {
221: if ($name === '') {
222: return false;
223: }
224:
225: return $this->betterReflectionEnum->hasCase($name) || $this->betterReflectionEnum->hasConstant($name);
226: }
227:
228: /**
229: * @param int-mask-of<ReflectionClassConstant::IS_*>|null $filter
230: *
231: * @return array<non-empty-string, mixed>
232: */
233: public function getConstants(int|null $filter = null): array
234: {
235: /** @psalm-suppress ImpureFunctionCall */
236: return array_map(
237: fn (BetterReflectionClassConstant|BetterReflectionEnumCase $betterConstantOrEnumCase): mixed => $this->getConstantValue($betterConstantOrEnumCase),
238: $this->filterBetterReflectionClassConstants($filter),
239: );
240: }
241:
242: public function getConstant(string $name): mixed
243: {
244: if ($name === '') {
245: return false;
246: }
247:
248: $enumCase = $this->betterReflectionEnum->getCase($name);
249: if ($enumCase !== null) {
250: return $this->getConstantValue($enumCase);
251: }
252:
253: $betterReflectionConstant = $this->betterReflectionEnum->getConstant($name);
254: if ($betterReflectionConstant === null) {
255: return false;
256: }
257:
258: return $betterReflectionConstant->getValue();
259: }
260:
261: private function getConstantValue(BetterReflectionClassConstant|BetterReflectionEnumCase $betterConstantOrEnumCase): mixed
262: {
263: if ($betterConstantOrEnumCase instanceof BetterReflectionEnumCase) {
264: throw new Exception\NotImplemented('Not implemented');
265: }
266:
267: return $betterConstantOrEnumCase->getValue();
268: }
269:
270: public function getReflectionConstant(string $name): ReflectionClassConstant|false
271: {
272: if ($name === '') {
273: return false;
274: }
275:
276: // @infection-ignore-all Coalesce: There's no difference
277: $betterReflectionConstantOrEnumCase = $this->betterReflectionEnum->getCase($name) ?? $this->betterReflectionEnum->getConstant($name);
278: if ($betterReflectionConstantOrEnumCase === null) {
279: return false;
280: }
281:
282: return new ReflectionClassConstant($betterReflectionConstantOrEnumCase);
283: }
284:
285: /**
286: * @param int-mask-of<ReflectionClassConstant::IS_*>|null $filter
287: *
288: * @return list<ReflectionClassConstant>
289: */
290: public function getReflectionConstants(int|null $filter = null): array
291: {
292: return array_values(array_map(
293: static fn (BetterReflectionClassConstant|BetterReflectionEnumCase $betterConstantOrEnum): ReflectionClassConstant => new ReflectionClassConstant($betterConstantOrEnum),
294: $this->filterBetterReflectionClassConstants($filter),
295: ));
296: }
297:
298: /**
299: * @param int-mask-of<ReflectionClassConstant::IS_*>|null $filter
300: *
301: * @return array<non-empty-string, BetterReflectionClassConstant|BetterReflectionEnumCase>
302: */
303: private function filterBetterReflectionClassConstants(int|null $filter): array
304: {
305: $reflectionConstants = $this->betterReflectionEnum->getConstants($filter ?? 0);
306:
307: if (
308: $filter === null
309: || $filter & ReflectionClassConstant::IS_PUBLIC_COMPATIBILITY
310: ) {
311: $reflectionConstants += $this->betterReflectionEnum->getCases();
312: }
313:
314: return $reflectionConstants;
315: }
316:
317: /** @return array<class-string, ReflectionClass> */
318: public function getInterfaces(): array
319: {
320: /** @psalm-suppress ImpureFunctionCall */
321: return array_map(
322: static fn (BetterReflectionClass $interface): ReflectionClass => new ReflectionClass($interface),
323: $this->betterReflectionEnum->getInterfaces(),
324: );
325: }
326:
327: /** @return list<class-string> */
328: public function getInterfaceNames(): array
329: {
330: return $this->betterReflectionEnum->getInterfaceNames();
331: }
332:
333: public function isInterface(): bool
334: {
335: return $this->betterReflectionEnum->isInterface();
336: }
337:
338: /** @return array<trait-string, ReflectionClass> */
339: public function getTraits(): array
340: {
341: $traits = $this->betterReflectionEnum->getTraits();
342:
343: $traitsByName = [];
344: foreach ($traits as $trait) {
345: /** @var trait-string $traitName */
346: $traitName = $trait->getName();
347: $traitsByName[$traitName] = new ReflectionClass($trait);
348: }
349:
350: return $traitsByName;
351: }
352:
353: /** @return list<trait-string> */
354: public function getTraitNames(): array
355: {
356: return $this->betterReflectionEnum->getTraitNames();
357: }
358:
359: /** @return array<non-empty-string, non-empty-string> */
360: public function getTraitAliases(): array
361: {
362: return $this->betterReflectionEnum->getTraitAliases();
363: }
364:
365: public function isTrait(): bool
366: {
367: return $this->betterReflectionEnum->isTrait();
368: }
369:
370: public function isAbstract(): bool
371: {
372: return $this->betterReflectionEnum->isAbstract();
373: }
374:
375: public function isFinal(): bool
376: {
377: return $this->betterReflectionEnum->isFinal();
378: }
379:
380: public function isReadOnly(): bool
381: {
382: return $this->betterReflectionEnum->isReadOnly();
383: }
384:
385: public function getModifiers(): int
386: {
387: return $this->betterReflectionEnum->getModifiers();
388: }
389:
390: public function isInstance(object $object): bool
391: {
392: return $this->betterReflectionEnum->isInstance($object);
393: }
394:
395: /** @return never */
396: public function newInstance(mixed ...$args): object
397: {
398: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
399: }
400:
401: /** @return never */
402: public function newInstanceWithoutConstructor(): object
403: {
404: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
405: }
406:
407: /** @return never */
408: public function newInstanceArgs(array|null $args = null): object
409: {
410: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
411: }
412:
413: /**
414: * @param int-mask-of<ReflectionClass::SKIP_*> $options
415: *
416: * @return never
417: */
418: public function newLazyGhost(callable $initializer, int $options = 0): object
419: {
420: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
421: }
422:
423: /**
424: * @param int-mask-of<ReflectionClass::SKIP_*> $options
425: *
426: * @return never
427: */
428: public function newLazyProxy(callable $factory, int $options = 0): object
429: {
430: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
431: }
432:
433: /** @return never */
434: public function markLazyObjectAsInitialized(object $object): object
435: {
436: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
437: }
438:
439: /** @return never */
440: public function getLazyInitializer(object $object): callable|null
441: {
442: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
443: }
444:
445: /** @return never */
446: public function initializeLazyObject(object $object): object
447: {
448: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
449: }
450:
451: /** @return never */
452: public function isUninitializedLazyObject(object $object): bool
453: {
454: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
455: }
456:
457: /** @param int-mask-of<ReflectionClass::SKIP_*> $options */
458: public function resetAsLazyGhost(object $object, callable $initializer, int $options = 0): void
459: {
460: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
461: }
462:
463: /** @param int-mask-of<ReflectionClass::SKIP_*> $options */
464: public function resetAsLazyProxy(object $object, callable $factory, int $options = 0): void
465: {
466: throw Exception\NotImplementedBecauseItTriggersAutoloading::create();
467: }
468:
469: public function getParentClass(): ReflectionClass|false
470: {
471: return false;
472: }
473:
474: public function isSubclassOf(CoreReflectionClass|string $class): bool
475: {
476: $realParentClassNames = $this->betterReflectionEnum->getParentClassNames();
477:
478: $parentClassNames = array_combine(array_map(static fn (string $parentClassName): string => strtolower($parentClassName), $realParentClassNames), $realParentClassNames);
479:
480: $className = $class instanceof CoreReflectionClass ? $class->getName() : $class;
481: $lowercasedClassName = strtolower($className);
482:
483: $realParentClassName = $parentClassNames[$lowercasedClassName] ?? $className;
484:
485: if ($this->betterReflectionEnum->isSubclassOf($realParentClassName)) {
486: return true;
487: }
488:
489: return $this->implementsInterface($className);
490: }
491:
492: /**
493: * @return array<string, mixed>
494: *
495: * @psalm-suppress LessSpecificImplementedReturnType
496: */
497: public function getStaticProperties(): array
498: {
499: return $this->betterReflectionEnum->getStaticProperties();
500: }
501:
502: public function getStaticPropertyValue(string $name, mixed $default = null): mixed
503: {
504: throw new CoreReflectionException(sprintf('Property %s::$%s does not exist', $this->betterReflectionEnum->getName(), $name));
505: }
506:
507: public function setStaticPropertyValue(string $name, mixed $value): void
508: {
509: throw new CoreReflectionException(sprintf('Class %s does not have a property named %s', $this->betterReflectionEnum->getName(), $name));
510: }
511:
512: /** @return array<non-empty-string, mixed> */
513: public function getDefaultProperties(): array
514: {
515: return $this->betterReflectionEnum->getDefaultProperties();
516: }
517:
518: public function isIterateable(): bool
519: {
520: return $this->betterReflectionEnum->isIterateable();
521: }
522:
523: public function isIterable(): bool
524: {
525: return $this->isIterateable();
526: }
527:
528: public function implementsInterface(CoreReflectionClass|string $interface): bool
529: {
530: $realInterfaceNames = $this->betterReflectionEnum->getInterfaceNames();
531:
532: $interfaceNames = array_combine(array_map(static fn (string $interfaceName): string => strtolower($interfaceName), $realInterfaceNames), $realInterfaceNames);
533:
534: $interfaceName = $interface instanceof CoreReflectionClass ? $interface->getName() : $interface;
535: $lowercasedInterfaceName = strtolower($interfaceName);
536:
537: $realInterfaceName = $interfaceNames[$lowercasedInterfaceName] ?? $interfaceName;
538:
539: return $this->betterReflectionEnum->implementsInterface($realInterfaceName);
540: }
541:
542: public function getExtension(): ?CoreReflectionExtension
543: {
544: throw new Exception\NotImplemented('Not implemented');
545: }
546:
547: /** @return non-empty-string|false */
548: public function getExtensionName(): string|false
549: {
550: return $this->betterReflectionEnum->getExtensionName() ?? false;
551: }
552:
553: public function inNamespace(): bool
554: {
555: return $this->betterReflectionEnum->inNamespace();
556: }
557:
558: public function getNamespaceName(): string
559: {
560: return $this->betterReflectionEnum->getNamespaceName() ?? '';
561: }
562:
563: public function getShortName(): string
564: {
565: return $this->betterReflectionEnum->getShortName();
566: }
567:
568: /**
569: * @param class-string|null $name
570: *
571: * @return list<ReflectionAttribute|FakeReflectionAttribute>
572: */
573: public function getAttributes(string|null $name = null, int $flags = 0): array
574: {
575: if ($flags !== 0 && $flags !== ReflectionAttribute::IS_INSTANCEOF) {
576: throw new ValueError('Argument #2 ($flags) must be a valid attribute filter flag');
577: }
578:
579: if ($name !== null && $flags !== 0) {
580: $attributes = $this->betterReflectionEnum->getAttributesByInstance($name);
581: } elseif ($name !== null) {
582: $attributes = $this->betterReflectionEnum->getAttributesByName($name);
583: } else {
584: $attributes = $this->betterReflectionEnum->getAttributes();
585: }
586:
587: /** @psalm-suppress ImpureFunctionCall */
588: return array_map(static fn (BetterReflectionAttribute $betterReflectionAttribute): ReflectionAttribute|FakeReflectionAttribute => ReflectionAttributeFactory::create($betterReflectionAttribute), $attributes);
589: }
590:
591: public function isEnum(): bool
592: {
593: return $this->betterReflectionEnum->isEnum();
594: }
595:
596: public function hasCase(string $name): bool
597: {
598: if ($name === '') {
599: return false;
600: }
601:
602: return $this->betterReflectionEnum->hasCase($name);
603: }
604:
605: public function getCase(string $name): ReflectionEnumUnitCase|ReflectionEnumBackedCase
606: {
607: $case = $name !== '' ? $this->betterReflectionEnum->getCase($name) : null;
608:
609: if ($case === null) {
610: throw new CoreReflectionException(sprintf('Case %s::%s does not exist', $this->betterReflectionEnum->getName(), $name));
611: }
612:
613: if ($this->betterReflectionEnum->isBacked()) {
614: return new ReflectionEnumBackedCase($case);
615: }
616:
617: return new ReflectionEnumUnitCase($case);
618: }
619:
620: /** @return list<ReflectionEnumUnitCase>|list<ReflectionEnumBackedCase> */
621: public function getCases(): array
622: {
623: return $this->cases->get();
624: }
625:
626: public function isBacked(): bool
627: {
628: return $this->betterReflectionEnum->isBacked();
629: }
630:
631: public function getBackingType(): ReflectionNamedType|null
632: {
633: if ($this->betterReflectionEnum->isBacked()) {
634: return new ReflectionNamedType($this->betterReflectionEnum->getBackingType(), false);
635: }
636:
637: return null;
638: }
639: }
640: