1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\Reflection;
6:
7: use BackedEnum;
8: use PhpParser\Modifiers;
9: use PhpParser\Node;
10: use PhpParser\Node\Stmt\Class_ as ClassNode;
11: use PhpParser\Node\Stmt\ClassMethod;
12: use PhpParser\Node\Stmt\Enum_ as EnumNode;
13: use PhpParser\Node\Stmt\Interface_ as InterfaceNode;
14: use PhpParser\Node\Stmt\Trait_ as TraitNode;
15: use PhpParser\Node\Stmt\TraitUse;
16: use ReflectionClass as CoreReflectionClass;
17: use ReflectionException;
18: use ReflectionMethod as CoreReflectionMethod;
19: use PHPStan\BetterReflection\BetterReflection;
20: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass as ReflectionClassAdapter;
21: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClassConstant as ReflectionClassConstantAdapter;
22: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod as ReflectionMethodAdapter;
23: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty as ReflectionPropertyAdapter;
24: use PHPStan\BetterReflection\Reflection\Attribute\ReflectionAttributeHelper;
25: use PHPStan\BetterReflection\Reflection\Deprecated\DeprecatedHelper;
26: use PHPStan\BetterReflection\Reflection\Exception\ClassDoesNotExist;
27: use PHPStan\BetterReflection\Reflection\Exception\NoObjectProvided;
28: use PHPStan\BetterReflection\Reflection\Exception\NotAnObject;
29: use PHPStan\BetterReflection\Reflection\Exception\ObjectNotInstanceOfClass;
30: use PHPStan\BetterReflection\Reflection\Exception\PropertyDoesNotExist;
31: use PHPStan\BetterReflection\Reflection\StringCast\ReflectionClassStringCast;
32: use PHPStan\BetterReflection\Reflection\Support\AlreadyVisitedClasses;
33: use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound;
34: use PHPStan\BetterReflection\Reflector\Reflector;
35: use PHPStan\BetterReflection\SourceLocator\Located\InternalLocatedSource;
36: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
37: use PHPStan\BetterReflection\Util\CalculateReflectionColumn;
38: use PHPStan\BetterReflection\Util\GetLastDocComment;
39: use Stringable;
40: use Traversable;
41: use UnitEnum;
42:
43: use function array_combine;
44: use function array_filter;
45: use function array_key_exists;
46: use function array_keys;
47: use function array_map;
48: use function array_merge;
49: use function array_pop;
50: use function array_reverse;
51: use function array_slice;
52: use function array_values;
53: use function assert;
54: use function in_array;
55: use function is_int;
56: use function is_string;
57: use function ltrim;
58: use function sha1;
59: use function sprintf;
60: use function strtolower;
61:
62: /** @psalm-immutable */
63: class ReflectionClass implements Reflection
64: {
65: private Reflector $reflector;
66: private LocatedSource $locatedSource;
67: /**
68: * @var non-empty-string|null
69: */
70: private $namespace = null;
71: /**
72: * @var string
73: */
74: public const ANONYMOUS_CLASS_NAME_PREFIX = 'class@anonymous';
75: /**
76: * @var string
77: */
78: public const ANONYMOUS_CLASS_NAME_PREFIX_REGEXP = '~^(?:class|[\w\\\\]+)@anonymous~';
79: /**
80: * @var string
81: */
82: private const ANONYMOUS_CLASS_NAME_SUFFIX = '@anonymous';
83:
84: /** @var class-string|trait-string|null */
85: private $name;
86:
87: /** @var non-empty-string|null */
88: private $shortName;
89:
90: private bool $isInterface;
91: private bool $isTrait;
92: private bool $isEnum;
93: private bool $isBackedEnum;
94:
95: /** @var int-mask-of<ReflectionClassAdapter::IS_*> */
96: private int $modifiers;
97:
98: /** @var non-empty-string|null */
99: private $docComment;
100:
101: /** @var list<ReflectionAttribute> */
102: private array $attributes;
103:
104: /** @var positive-int */
105: private int $startLine;
106:
107: /** @var positive-int */
108: private int $endLine;
109:
110: /** @var positive-int */
111: private int $startColumn;
112:
113: /** @var positive-int */
114: private int $endColumn;
115:
116: /** @var class-string|null */
117: private $parentClassName;
118:
119: /** @var list<class-string> */
120: private array $implementsClassNames;
121:
122: /** @var list<trait-string> */
123: private array $traitClassNames;
124:
125: /** @var array<non-empty-string, ReflectionClassConstant> */
126: private array $immediateConstants;
127:
128: /** @var array<non-empty-string, ReflectionProperty> */
129: private array $immediateProperties;
130:
131: /** @var array<non-empty-string, ReflectionMethod> */
132: private array $immediateMethods;
133:
134: /** @var array{
135: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
136: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
137: * precedences: array<non-empty-string, non-empty-string>,
138: * hashes: array<non-empty-string, non-empty-string>,
139: * }
140: */
141: private array $traitsData;
142:
143: /**
144: * @var array<non-empty-string, ReflectionClassConstant>|null
145: * @psalm-allow-private-mutation
146: */
147: private $cachedConstants = null;
148:
149: /**
150: * @var array<non-empty-string, ReflectionProperty>|null
151: * @psalm-allow-private-mutation
152: */
153: private $cachedProperties = null;
154:
155: /** @var array<class-string, ReflectionClass>|null */
156: private $cachedInterfaces = null;
157:
158: /** @var list<class-string>|null */
159: private $cachedInterfaceNames = null;
160:
161: /** @var list<ReflectionClass>|null */
162: private $cachedTraits = null;
163:
164: /**
165: * @var \PHPStan\BetterReflection\Reflection\ReflectionMethod|null
166: */
167: private $cachedConstructor = null;
168:
169: /**
170: * @var string|null
171: */
172: private $cachedName = null;
173:
174: /**
175: * @psalm-allow-private-mutation
176: * @var array<lowercase-string, ReflectionMethod>|null
177: */
178: private $cachedMethods = null;
179:
180: /**
181: * @var list<ReflectionClass>|null
182: * @psalm-allow-private-mutation
183: */
184: private $cachedParentClasses = null;
185:
186: /** @psalm-allow-private-mutation
187: * @var bool|null */
188: private $cachedIsOnParentClassCycle = null;
189:
190: /** @psalm-allow-private-mutation
191: * @var bool|null */
192: private $cachedIsOnInterfaceCycle = null;
193:
194: /** @psalm-allow-private-mutation
195: * @var bool|null */
196: private $cachedIsOnTraitCycle = null;
197:
198: /**
199: * @internal
200: *
201: * @param non-empty-string|null $namespace
202: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
203: */
204: protected function __construct(
205: Reflector $reflector,
206: $node,
207: LocatedSource $locatedSource,
208: ?string $namespace = null
209: ) {
210: $this->reflector = $reflector;
211: $this->locatedSource = $locatedSource;
212: $this->namespace = $namespace;
213: $name = null;
214: $shortName = null;
215:
216: if ($node->name instanceof Node\Identifier) {
217: $namespacedName = $node->namespacedName;
218: if ($namespacedName === null) {
219: /** @psalm-var class-string|trait-string */
220: $name = $node->name->name;
221: } else {
222: /** @psalm-var class-string|trait-string */
223: $name = $namespacedName->toString();
224: }
225:
226: $shortName = $node->name->name;
227: }
228:
229: $this->name = $name;
230: $this->shortName = $shortName;
231: $this->isInterface = $node instanceof InterfaceNode;
232: $this->isTrait = $node instanceof TraitNode;
233: $this->isEnum = $node instanceof EnumNode;
234: $this->isBackedEnum = $node instanceof EnumNode && $node->scalarType !== null;
235:
236: $this->modifiers = $this->computeModifiers($node);
237: $this->docComment = GetLastDocComment::forNode($node);
238: $this->attributes = ReflectionAttributeHelper::createAttributes($reflector, $this, $node->attrGroups);
239:
240: $startLine = $node->getStartLine();
241: assert($startLine > 0);
242: $endLine = $node->getEndLine();
243: assert($endLine > 0);
244:
245: $this->startLine = $startLine;
246: $this->endLine = $endLine;
247: $this->startColumn = CalculateReflectionColumn::getStartColumn($locatedSource->getSource(), $node);
248: $this->endColumn = CalculateReflectionColumn::getEndColumn($locatedSource->getSource(), $node);
249:
250: /** @var class-string|null $parentClassName */
251: $parentClassName = $node instanceof ClassNode ? ($nullsafeVariable1 = $node->extends) ? $nullsafeVariable1->toString() : null : null;
252: $this->parentClassName = $parentClassName;
253:
254: // @infection-ignore-all UnwrapArrayMap: It works without array_map() as well but this is less magical
255: /** @var list<class-string> $implementsClassNames */
256: $implementsClassNames = array_map(
257: static fn (Node\Name $name): string => $name->toString(),
258: $node instanceof TraitNode ? [] : ($node instanceof InterfaceNode ? $node->extends : $node->implements),
259: );
260: $this->implementsClassNames = $implementsClassNames;
261:
262: /** @var list<trait-string> $traitClassNames */
263: $traitClassNames = array_merge(
264: [],
265: ...array_map(
266: // @infection-ignore-all UnwrapArrayMap: It works without array_map() as well but this is less magical
267: static fn (TraitUse $traitUse): array => array_map(static fn (Node\Name $traitName): string => $traitName->toString(), $traitUse->traits),
268: $node->getTraitUses(),
269: ),
270: );
271: $this->traitClassNames = $traitClassNames;
272:
273: $this->immediateConstants = $this->createImmediateConstants($node, $reflector);
274: $this->immediateProperties = $this->createImmediateProperties($node, $reflector);
275: $this->immediateMethods = $this->createImmediateMethods($node, $reflector);
276:
277: $this->traitsData = $this->computeTraitsData($node);
278: }
279:
280: /**
281: * @return array<string, mixed>
282: */
283: public function exportToCache(): array
284: {
285: return [
286: 'locatedSource' => $this->locatedSource->exportToCache(),
287: 'namespace' => $this->namespace,
288: 'name' => $this->name,
289: 'shortName' => $this->shortName,
290: 'isInterface' => $this->isInterface,
291: 'isTrait' => $this->isTrait,
292: 'isEnum' => $this->isEnum,
293: 'isBackedEnum' => $this->isBackedEnum,
294: 'modifiers' => $this->modifiers,
295: 'docComment' => $this->docComment,
296: 'attributes' => array_map(static fn ($ref) => $ref->exportToCache(), $this->attributes),
297: 'startLine' => $this->startLine,
298: 'endLine' => $this->endLine,
299: 'startColumn' => $this->startColumn,
300: 'endColumn' => $this->endColumn,
301: 'parentClassName' => $this->parentClassName,
302: 'implementsClassNames' => $this->implementsClassNames,
303: 'traitClassNames' => $this->traitClassNames,
304: 'immediateConstants' => array_map(static fn ($ref) => $ref->exportToCache(), $this->immediateConstants),
305: 'immediateProperties' => array_map(static fn ($ref) => $ref->exportToCache(), $this->immediateProperties),
306: 'immediateMethods' => array_map(static fn ($ref) => $ref->exportToCache(), $this->immediateMethods),
307: 'traitsData' => $this->traitsData,
308: ];
309: }
310:
311: /**
312: * @param array<string, mixed> $data
313: * @return static
314: */
315: public static function importFromCache(Reflector $reflector, array $data): self
316: {
317: $reflection = new CoreReflectionClass(static::class);
318: /** @var static $ref */
319: $ref = $reflection->newInstanceWithoutConstructor();
320: $ref->reflector = $reflector;
321:
322: $locatedSource = LocatedSource::importFromCache($data['locatedSource']);
323: $ref->locatedSource = $locatedSource;
324: $ref->namespace = $data['namespace'];
325: $ref->name = $data['name'];
326: $ref->shortName = $data['shortName'];
327: $ref->isInterface = $data['isInterface'];
328: $ref->isTrait = $data['isTrait'];
329: $ref->isEnum = $data['isEnum'];
330: $ref->isBackedEnum = $data['isBackedEnum'];
331: $ref->modifiers = $data['modifiers'];
332: $ref->docComment = $data['docComment'];
333: $ref->attributes = array_map(
334: static fn ($constData) => ReflectionAttribute::importFromCache($reflector, $constData, $ref),
335: $data['attributes'],
336: );
337: $ref->startLine = $data['startLine'];
338: $ref->endLine = $data['endLine'];
339: $ref->startColumn = $data['startColumn'];
340: $ref->endColumn = $data['endColumn'];
341: $ref->parentClassName = $data['parentClassName'];
342: $ref->implementsClassNames = $data['implementsClassNames'];
343: $ref->traitClassNames = $data['traitClassNames'];
344: $ref->immediateConstants = array_map(
345: static fn ($constData) => ReflectionClassConstant::importFromCache($reflector, $constData),
346: $data['immediateConstants'],
347: );
348: $ref->immediateProperties = array_map(
349: static fn ($propData) => ReflectionProperty::importFromCache($reflector, $propData, $locatedSource),
350: $data['immediateProperties'],
351: );
352: $ref->immediateMethods = array_map(
353: static fn ($methodData) => ReflectionMethod::importFromCache($reflector, $methodData, $locatedSource, null),
354: $data['immediateMethods'],
355: );
356: $ref->traitsData = $data['traitsData'];
357:
358: return $ref;
359: }
360:
361: /** @return non-empty-string */
362: public function __toString(): string
363: {
364: return ReflectionClassStringCast::toString($this);
365: }
366:
367: /**
368: * Create a ReflectionClass from an instance, using default reflectors etc.
369: *
370: * This is simply a helper method that calls ReflectionObject::createFromInstance().
371: *
372: * @see ReflectionObject::createFromInstance
373: *
374: * @throws IdentifierNotFound
375: * @throws ReflectionException
376: */
377: public static function createFromInstance(object $instance): self
378: {
379: return ReflectionObject::createFromInstance($instance);
380: }
381:
382: /**
383: * Create from a Class Node.
384: *
385: * @internal
386: *
387: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
388: * @param non-empty-string|null $namespace optional - if omitted, we assume it is global namespaced class
389: */
390: public static function createFromNode(
391: Reflector $reflector,
392: $node,
393: LocatedSource $locatedSource,
394: ?string $namespace = null
395: ): self {
396: return new self($reflector, $node, $locatedSource, $namespace);
397: }
398:
399: /**
400: * Get the "short" name of the class (e.g. for A\B\Foo, this will return
401: * "Foo").
402: *
403: * @return non-empty-string
404: */
405: public function getShortName(): string
406: {
407: if ($this->shortName !== null) {
408: return $this->shortName;
409: }
410:
411: $fileName = $this->getFileName();
412:
413: if ($fileName === null) {
414: $fileName = sha1($this->locatedSource->getSource());
415: }
416:
417: return sprintf('%s%s%c%s(%d)', $this->getAnonymousClassNamePrefix(), self::ANONYMOUS_CLASS_NAME_SUFFIX, "\0", $fileName, $this->getStartLine());
418: }
419:
420: /**
421: * PHP creates the name of the anonymous class based on first parent
422: * or implemented interface.
423: */
424: private function getAnonymousClassNamePrefix(): string
425: {
426: if ($this->parentClassName !== null) {
427: return $this->parentClassName;
428: }
429:
430: $implementsClassName = $this->implementsClassNames;
431: if ($implementsClassName !== []) {
432: return $implementsClassName[0];
433: }
434:
435: return 'class';
436: }
437:
438: /**
439: * Get the "full" name of the class (e.g. for A\B\Foo, this will return
440: * "A\B\Foo").
441: *
442: * @return class-string|trait-string
443: */
444: public function getName(): string
445: {
446: if ($this->cachedName !== null) {
447: return $this->cachedName;
448: }
449:
450: if (! $this->inNamespace()) {
451: /** @psalm-var class-string|trait-string */
452: return $this->cachedName = $this->getShortName();
453: }
454:
455: assert($this->name !== null);
456:
457: return $this->cachedName = $this->name;
458: }
459:
460: /** @return class-string|null */
461: public function getParentClassName(): ?string
462: {
463: return $this->parentClassName;
464: }
465:
466: /**
467: * Get the "namespace" name of the class (e.g. for A\B\Foo, this will
468: * return "A\B").
469: *
470: * @return non-empty-string|null
471: */
472: public function getNamespaceName(): ?string
473: {
474: return $this->namespace;
475: }
476:
477: /**
478: * Decide if this class is part of a namespace. Returns false if the class
479: * is in the global namespace or does not have a specified namespace.
480: */
481: public function inNamespace(): bool
482: {
483: return $this->namespace !== null;
484: }
485:
486: /** @return non-empty-string|null */
487: public function getExtensionName(): ?string
488: {
489: return $this->locatedSource->getExtensionName();
490: }
491:
492: /**
493: * @param array<lowercase-string, ReflectionMethod> $currentMethods
494: *
495: * @return list<ReflectionMethod>
496: */
497: private function createMethodsFromTrait(ReflectionMethod $method, array $currentMethods): array
498: {
499: $methodModifiers = $method->getModifiers();
500: $lowerCasedMethodHash = $this->lowerCasedMethodHash($method->getImplementingClass()->getName(), $method->getName());
501:
502: if (array_key_exists($lowerCasedMethodHash, $this->traitsData['modifiers'])) {
503: $newModifierAst = $this->traitsData['modifiers'][$lowerCasedMethodHash];
504: if ($this->traitsData['modifiers'][$lowerCasedMethodHash] & ClassNode::VISIBILITY_MODIFIER_MASK) {
505: $methodModifiersWithoutVisibility = $methodModifiers;
506: if (($methodModifiers & CoreReflectionMethod::IS_PUBLIC) === CoreReflectionMethod::IS_PUBLIC) {
507: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PUBLIC;
508: }
509: if (($methodModifiers & CoreReflectionMethod::IS_PROTECTED) === CoreReflectionMethod::IS_PROTECTED) {
510: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PROTECTED;
511: }
512: if (($methodModifiers & CoreReflectionMethod::IS_PRIVATE) === CoreReflectionMethod::IS_PRIVATE) {
513: $methodModifiersWithoutVisibility -= CoreReflectionMethod::IS_PRIVATE;
514: }
515: $newModifier = 0;
516: if (($newModifierAst & Modifiers::PUBLIC) === Modifiers::PUBLIC) {
517: $newModifier = CoreReflectionMethod::IS_PUBLIC;
518: }
519: if (($newModifierAst & Modifiers::PROTECTED) === Modifiers::PROTECTED) {
520: $newModifier = CoreReflectionMethod::IS_PROTECTED;
521: }
522: if (($newModifierAst & Modifiers::PRIVATE) === Modifiers::PRIVATE) {
523: $newModifier = CoreReflectionMethod::IS_PRIVATE;
524: }
525: $methodModifiers = $methodModifiersWithoutVisibility | $newModifier;
526: }
527: if (($newModifierAst & Modifiers::FINAL) === Modifiers::FINAL) {
528: $methodModifiers |= CoreReflectionMethod::IS_FINAL;
529: }
530: }
531:
532: $createMethod = function (?string $aliasMethodName, int $methodModifiers) use ($method): ReflectionMethod {
533: assert($aliasMethodName === null || $aliasMethodName !== '');
534:
535: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> $methodModifiers */
536: $methodModifiers = $methodModifiers;
537:
538: return $method->withImplementingClass($this, $aliasMethodName, $methodModifiers);
539: };
540:
541: $methods = [];
542:
543: if (
544: ! array_key_exists($lowerCasedMethodHash, $this->traitsData['precedences'])
545: && ! array_key_exists($lowerCasedMethodHash, $currentMethods)
546: ) {
547: $modifiersUsedWithAlias = false;
548:
549: foreach ($this->traitsData['aliases'] as $traitAliasDefinitions) {
550: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
551: if ($lowerCasedMethodHash === $traitAliasDefinition['hash']) {
552: $modifiersUsedWithAlias = true;
553: break;
554: }
555: }
556: }
557:
558: // Modifiers used with alias -> copy method with original modifiers (will be added later with the alias name and new modifiers)
559: // Modifiers not used with alias -> add method with new modifiers
560: $methods[] = $createMethod($method->getAliasName(), $modifiersUsedWithAlias ? $method->getModifiers() : $methodModifiers);
561: }
562:
563: if ($this->traitsData['aliases'] !== []) {
564: $traits = [];
565: foreach ($this->getTraits() as $trait) {
566: $traits[$trait->getName()] = $trait;
567: }
568:
569: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
570: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
571: if ($lowerCasedMethodHash !== $traitAliasDefinition['hash']) {
572: continue;
573: }
574:
575: if (!array_key_exists($traitClassName, $traits)) {
576: continue;
577: }
578:
579: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
580: continue;
581: }
582:
583: $methods[] = $createMethod($traitAliasDefinition['alias'], $methodModifiers);
584: }
585: }
586: }
587:
588: return $methods;
589: }
590:
591: /**
592: * Construct a flat list of all methods in this precise order from:
593: * - current class
594: * - parent class
595: * - traits used in parent class
596: * - interfaces implemented in parent class
597: * - traits used in current class
598: * - interfaces implemented in current class
599: *
600: * Methods are not merged via their name as array index, since internal PHP method
601: * sorting does not follow `\array_merge()` semantics.
602: *
603: * @return array<lowercase-string, ReflectionMethod> indexed by method name
604: */
605: private function getMethodsIndexedByLowercasedName(AlreadyVisitedClasses $alreadyVisitedClasses): array
606: {
607: if ($this->cachedMethods !== null) {
608: return $this->cachedMethods;
609: }
610:
611: $alreadyVisitedClasses->push($this->getName());
612:
613: $immediateMethods = $this->getImmediateMethods();
614: $className = $this->getName();
615:
616: $methods = array_combine(
617: array_map(static fn (ReflectionMethod $method): string => strtolower($method->getName()), $immediateMethods),
618: $immediateMethods,
619: );
620:
621: $parentClass = $this->getParentClass();
622: if ($parentClass !== null) {
623: foreach ($parentClass->getMethodsIndexedByLowercasedName($alreadyVisitedClasses) as $lowercasedMethodName => $method) {
624: if (array_key_exists($lowercasedMethodName, $methods)) {
625: continue;
626: }
627:
628: $methods[$lowercasedMethodName] = $method->withCurrentClass($this);
629: }
630: }
631:
632: foreach ($this->getTraits() as $trait) {
633: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
634: foreach ($trait->getMethodsIndexedByLowercasedName($alreadyVisitedClassesCopy) as $method) {
635: foreach ($this->createMethodsFromTrait($method, $methods) as $traitMethod) {
636: $lowercasedMethodName = strtolower($traitMethod->getName());
637:
638: if (! array_key_exists($lowercasedMethodName, $methods)) {
639: $methods[$lowercasedMethodName] = $traitMethod;
640: continue;
641: }
642:
643: if ($traitMethod->isAbstract()) {
644: continue;
645: }
646:
647: // Non-abstract trait method can overwrite existing method:
648: // - when existing method comes from parent class
649: // - when existing method comes from trait and is abstract
650:
651: $existingMethod = $methods[$lowercasedMethodName];
652:
653: if (
654: $existingMethod->getDeclaringClass()->getName() === $className
655: && ! (
656: $existingMethod->isAbstract()
657: && $existingMethod->getDeclaringClass()->isTrait()
658: )
659: ) {
660: continue;
661: }
662:
663: $methods[$lowercasedMethodName] = $traitMethod;
664: }
665: }
666: }
667:
668: foreach ($this->getImmediateInterfaces() as $interface) {
669: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
670: foreach ($interface->getMethodsIndexedByLowercasedName($alreadyVisitedClassesCopy) as $lowercasedMethodName => $method) {
671: if (array_key_exists($lowercasedMethodName, $methods)) {
672: continue;
673: }
674:
675: $methods[$lowercasedMethodName] = $method;
676: }
677: }
678:
679: $this->cachedMethods = $methods;
680:
681: return $this->cachedMethods;
682: }
683:
684: /**
685: * Fetch an array of all methods for this class.
686: *
687: * Filter the results to include only methods with certain attributes. Defaults
688: * to no filtering.
689: * Any combination of \ReflectionMethod::IS_STATIC,
690: * \ReflectionMethod::IS_PUBLIC,
691: * \ReflectionMethod::IS_PROTECTED,
692: * \ReflectionMethod::IS_PRIVATE,
693: * \ReflectionMethod::IS_ABSTRACT,
694: * \ReflectionMethod::IS_FINAL.
695: * For example if $filter = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_FINAL
696: * the only the final public methods will be returned
697: *
698: * @param int-mask-of<CoreReflectionMethod::IS_*> $filter
699: *
700: * @return array<non-empty-string, ReflectionMethod>
701: */
702: public function getMethods(int $filter = 0): array
703: {
704: $methods = $this->cachedMethods ?? $this->getMethodsIndexedByLowercasedName(AlreadyVisitedClasses::createEmpty());
705:
706: if ($filter !== 0) {
707: $methods = array_filter(
708: $methods,
709: static fn (ReflectionMethod $method): bool => (bool) ($filter & $method->getModifiers()),
710: );
711: }
712:
713: return array_combine(
714: array_map(static fn (ReflectionMethod $method): string => $method->getName(), $methods),
715: $methods,
716: );
717: }
718:
719: /**
720: * Get only the methods that this class implements (i.e. do not search
721: * up parent classes etc.)
722: *
723: * @see ReflectionClass::getMethods for the usage of $filter
724: *
725: * @param int-mask-of<CoreReflectionMethod::IS_*> $filter
726: *
727: * @return array<non-empty-string, ReflectionMethod>
728: */
729: public function getImmediateMethods(int $filter = 0): array
730: {
731: if ($filter === 0) {
732: return $this->immediateMethods;
733: }
734:
735: return array_filter(
736: $this->immediateMethods,
737: static fn (ReflectionMethod $method): bool => (bool) ($filter & $method->getModifiers()),
738: );
739: }
740:
741: /** @return array<non-empty-string, ReflectionMethod>
742: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
743: private function createImmediateMethods($node, Reflector $reflector): array
744: {
745: $methods = [];
746:
747: foreach ($node->getMethods() as $methodNode) {
748: $method = ReflectionMethod::createFromMethodNode(
749: $reflector,
750: $methodNode,
751: $this->locatedSource,
752: $this->getNamespaceName(),
753: $this,
754: $this,
755: $this,
756: );
757:
758: if (array_key_exists($method->getName(), $methods)) {
759: continue;
760: }
761:
762: $methods[$method->getName()] = $method;
763: }
764:
765: if ($node instanceof EnumNode) {
766: $methods = $this->addEnumMethods($node, $methods);
767: }
768:
769: return $methods;
770: }
771:
772: /**
773: * @param array<non-empty-string, ReflectionMethod> $methods
774: *
775: * @return array<non-empty-string, ReflectionMethod>
776: */
777: private function addEnumMethods(EnumNode $node, array $methods): array
778: {
779: $internalLocatedSource = new InternalLocatedSource('', $this->getName(), 'Core', $this->getFileName());
780: $createMethod = function (string $name, array $params, $returnType) use ($internalLocatedSource): ReflectionMethod {
781: assert($name !== '');
782:
783: /** @var array{flags: int, params: Node\Param[], returnType: Node\Identifier|Node\NullableType} $classMethodSubnodes */
784: $classMethodSubnodes = [
785: 'flags' => Modifiers::PUBLIC | Modifiers::STATIC,
786: 'params' => $params,
787: 'returnType' => $returnType,
788: ];
789:
790: return ReflectionMethod::createFromMethodNode(
791: $this->reflector,
792: new ClassMethod(
793: new Node\Identifier($name),
794: $classMethodSubnodes,
795: ),
796: $internalLocatedSource,
797: $this->getNamespaceName(),
798: $this,
799: $this,
800: $this,
801: );
802: };
803:
804: $methods['cases'] = $createMethod('cases', [], new Node\Identifier('array'));
805:
806: if ($node->scalarType === null) {
807: return $methods;
808: }
809:
810: $valueParameter = new Node\Param(
811: new Node\Expr\Variable('value'),
812: null,
813: new Node\UnionType([new Node\Identifier('string'), new Node\Identifier('int')]),
814: );
815:
816: $methods['from'] = $createMethod(
817: 'from',
818: [$valueParameter],
819: new Node\Identifier('static'),
820: );
821:
822: $methods['tryFrom'] = $createMethod(
823: 'tryFrom',
824: [$valueParameter],
825: new Node\NullableType(new Node\Identifier('static')),
826: );
827:
828: return $methods;
829: }
830:
831: /**
832: * Get a single method with the name $methodName.
833: *
834: * @param non-empty-string $methodName
835: */
836: public function getMethod(string $methodName): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
837: {
838: $methods = $this->cachedMethods ?? $this->getMethodsIndexedByLowercasedName(AlreadyVisitedClasses::createEmpty());
839:
840: return $methods[strtolower($methodName)] ?? null;
841: }
842:
843: /**
844: * Does the class have the specified method?
845: *
846: * @param non-empty-string $methodName
847: */
848: public function hasMethod(string $methodName): bool
849: {
850: return $this->getMethod($methodName) !== null;
851: }
852:
853: /**
854: * Get an associative array of only the constants for this specific class (i.e. do not search
855: * up parent classes etc.), with keys as constant names and values as {@see ReflectionClassConstant} objects.
856: *
857: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
858: *
859: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
860: */
861: public function getImmediateConstants(int $filter = 0): array
862: {
863: if ($filter === 0) {
864: return $this->immediateConstants;
865: }
866:
867: return array_filter(
868: $this->immediateConstants,
869: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
870: );
871: }
872:
873: /**
874: * Does this class have the specified constant?
875: *
876: * @param non-empty-string $name
877: */
878: public function hasConstant(string $name): bool
879: {
880: return $this->getConstant($name) !== null;
881: }
882:
883: /**
884: * Get the reflection object of the specified class constant.
885: *
886: * Returns null if not specified.
887: *
888: * @param non-empty-string $name
889: */
890: public function getConstant(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionClassConstant
891: {
892: return $this->getConstants()[$name] ?? null;
893: }
894:
895: /** @return array<non-empty-string, ReflectionClassConstant>
896: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
897: private function createImmediateConstants($node, Reflector $reflector): array
898: {
899: $constants = [];
900:
901: foreach ($node->getConstants() as $constantsNode) {
902: foreach (array_keys($constantsNode->consts) as $constantPositionInNode) {
903: assert(is_int($constantPositionInNode));
904: $constant = ReflectionClassConstant::createFromNode($reflector, $constantsNode, $constantPositionInNode, $this, $this);
905:
906: $constants[$constant->getName()] = $constant;
907: }
908: }
909:
910: return $constants;
911: }
912:
913: /**
914: * Get an associative array of the defined constants in this class,
915: * with keys as constant names and values as {@see ReflectionClassConstant} objects.
916: *
917: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
918: *
919: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
920: */
921: public function getConstants(int $filter = 0): array
922: {
923: $constants = $this->cachedConstants ?? $this->getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
924:
925: if ($filter === 0) {
926: return $constants;
927: }
928:
929: return array_filter(
930: $constants,
931: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
932: );
933: }
934:
935: /** @return array<non-empty-string, ReflectionClassConstant> indexed by name */
936: private function getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
937: {
938: if ($this->cachedConstants !== null) {
939: return $this->cachedConstants;
940: }
941:
942: $alreadyVisitedClasses->push($this->getName());
943:
944: // Note: constants are not merged via their name as array index, since internal PHP constant
945: // sorting does not follow `\array_merge()` semantics
946:
947: $constants = $this->getImmediateConstants();
948:
949: $parentClass = $this->getParentClass();
950: if ($parentClass !== null) {
951: foreach ($parentClass->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
952: if ($constant->isPrivate()) {
953: continue;
954: }
955:
956: if (array_key_exists($constantName, $constants)) {
957: continue;
958: }
959:
960: $constants[$constantName] = $constant;
961: }
962: }
963:
964: foreach ($this->getTraits() as $trait) {
965: foreach ($trait->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
966: if (array_key_exists($constantName, $constants)) {
967: continue;
968: }
969:
970: $constants[$constantName] = $constant->withImplementingClass($this);
971: }
972: }
973:
974: foreach ($this->getImmediateInterfaces() as $interface) {
975: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
976: foreach ($interface->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClassesCopy) as $constantName => $constant) {
977: if (array_key_exists($constantName, $constants)) {
978: continue;
979: }
980:
981: $constants[$constantName] = $constant;
982: }
983: }
984:
985: $this->cachedConstants = $constants;
986:
987: return $this->cachedConstants;
988: }
989:
990: /**
991: * Get the constructor method for this class.
992: */
993: public function getConstructor(): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
994: {
995: if ($this->cachedConstructor !== null) {
996: return $this->cachedConstructor;
997: }
998:
999: $constructors = array_values(array_filter($this->getMethods(), static fn (ReflectionMethod $method): bool => $method->isConstructor()));
1000:
1001: return $this->cachedConstructor = $constructors[0] ?? null;
1002: }
1003:
1004: /**
1005: * Get only the properties for this specific class (i.e. do not search
1006: * up parent classes etc.)
1007: *
1008: * @see ReflectionClass::getProperties() for the usage of filter
1009: *
1010: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
1011: *
1012: * @return array<non-empty-string, ReflectionProperty>
1013: */
1014: public function getImmediateProperties(int $filter = 0): array
1015: {
1016: if ($filter === 0) {
1017: return $this->immediateProperties;
1018: }
1019:
1020: return array_filter(
1021: $this->immediateProperties,
1022: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
1023: );
1024: }
1025:
1026: /** @return array<non-empty-string, ReflectionProperty>
1027: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
1028: private function createImmediateProperties($node, Reflector $reflector): array
1029: {
1030: $properties = [];
1031:
1032: foreach ($node->getProperties() as $propertiesNode) {
1033: foreach ($propertiesNode->props as $propertyPropertyNode) {
1034: $property = ReflectionProperty::createFromNode(
1035: $reflector,
1036: $propertiesNode,
1037: $propertyPropertyNode,
1038: $this,
1039: $this,
1040: );
1041: $properties[$property->getName()] = $property;
1042: }
1043: }
1044:
1045: foreach ($node->getMethods() as $methodNode) {
1046: if ($methodNode->name->toLowerString() !== '__construct') {
1047: continue;
1048: }
1049:
1050: foreach ($methodNode->params as $parameterNode) {
1051: if ($parameterNode->flags === 0 && $parameterNode->hooks === []) {
1052: // No flags, no promotion
1053: continue;
1054: }
1055:
1056: $parameterNameNode = $parameterNode->var;
1057: assert($parameterNameNode instanceof Node\Expr\Variable);
1058: assert(is_string($parameterNameNode->name));
1059:
1060: $propertyNode = new Node\Stmt\Property(
1061: $parameterNode->flags,
1062: [new Node\PropertyItem($parameterNameNode->name, $parameterNode->default)],
1063: $parameterNode->getAttributes(),
1064: $parameterNode->type,
1065: $parameterNode->attrGroups,
1066: $parameterNode->hooks,
1067: );
1068: $property = ReflectionProperty::createFromNode(
1069: $reflector,
1070: $propertyNode,
1071: $propertyNode->props[0],
1072: $this,
1073: $this,
1074: true,
1075: );
1076: $properties[$property->getName()] = $property;
1077: }
1078: }
1079:
1080: if ($node instanceof EnumNode || $node instanceof InterfaceNode) {
1081: $properties = $this->addEnumProperties($properties, $node, $reflector);
1082: }
1083:
1084: return $properties;
1085: }
1086:
1087: /**
1088: * @param array<non-empty-string, ReflectionProperty> $properties
1089: *
1090: * @return array<non-empty-string, ReflectionProperty>
1091: * @param EnumNode|InterfaceNode $node
1092: */
1093: private function addEnumProperties(array $properties, $node, Reflector $reflector): array
1094: {
1095: $createProperty = function (string $name, $type) use ($reflector): ReflectionProperty {
1096: $propertyNode = new Node\Stmt\Property(
1097: Modifiers::PUBLIC | Modifiers::READONLY,
1098: [new Node\PropertyItem($name)],
1099: [],
1100: $type,
1101: );
1102:
1103: return ReflectionProperty::createFromNode(
1104: $reflector,
1105: $propertyNode,
1106: $propertyNode->props[0],
1107: $this,
1108: $this,
1109: );
1110: };
1111:
1112: if ($node instanceof InterfaceNode) {
1113: $interfaceName = $this->getName();
1114: if ($interfaceName === 'UnitEnum') {
1115: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
1116: }
1117:
1118: if ($interfaceName === 'BackedEnum') {
1119: $properties['value'] = $createProperty('value', new Node\UnionType([
1120: new Node\Identifier('int'),
1121: new Node\Identifier('string'),
1122: ]));
1123: }
1124: } else {
1125: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
1126:
1127: if ($node->scalarType !== null) {
1128: $properties['value'] = $createProperty('value', $node->scalarType);
1129: }
1130: }
1131:
1132: return $properties;
1133: }
1134:
1135: /**
1136: * Get the properties for this class.
1137: *
1138: * Filter the results to include only properties with certain attributes. Defaults
1139: * to no filtering.
1140: * Any combination of \ReflectionProperty::IS_STATIC,
1141: * \ReflectionProperty::IS_PUBLIC,
1142: * \ReflectionProperty::IS_PROTECTED,
1143: * \ReflectionProperty::IS_PRIVATE.
1144: * For example if $filter = \ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC
1145: * only the static public properties will be returned
1146: *
1147: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
1148: *
1149: * @return array<non-empty-string, ReflectionProperty>
1150: */
1151: public function getProperties(int $filter = 0): array
1152: {
1153: $properties = $this->cachedProperties ?? $this->getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
1154:
1155: if ($filter === 0) {
1156: return $properties;
1157: }
1158:
1159: return array_filter(
1160: $properties,
1161: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
1162: );
1163: }
1164:
1165: /** @return array<non-empty-string, ReflectionProperty> */
1166: private function getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
1167: {
1168: if ($this->cachedProperties !== null) {
1169: return $this->cachedProperties;
1170: }
1171:
1172: $alreadyVisitedClasses->push($this->getName());
1173:
1174: $immediateProperties = $this->getImmediateProperties();
1175:
1176: // Merging together properties from parent class, interfaces, traits, current class (in this precise order)
1177:
1178: $properties = array_merge(
1179: array_filter(
1180: (($nullsafeVariable2 = $this->getParentClass()) ? $nullsafeVariable2->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) : null) ?? [],
1181: static fn (ReflectionProperty $property) => ! $property->isPrivate(),
1182: ),
1183: ...array_map(
1184: static fn (ReflectionClass $ancestor): array => $ancestor->getPropertiesConsideringAlreadyVisitedClasses(clone $alreadyVisitedClasses),
1185: array_values($this->getImmediateInterfaces()),
1186: ),
1187: );
1188:
1189: foreach ($this->getTraits() as $trait) {
1190: foreach ($trait->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $traitProperty) {
1191: $traitPropertyName = $traitProperty->getName();
1192:
1193: $existingProperty = $immediateProperties[$traitPropertyName] ?? $properties[$traitPropertyName] ?? null;
1194:
1195: if ($existingProperty !== null && ! $existingProperty->isAbstract()) {
1196: continue;
1197: }
1198:
1199: $properties[$traitPropertyName] = $traitProperty->withImplementingClass($this);
1200: }
1201: }
1202:
1203: // Merge immediate properties last to get the required order
1204: $properties = array_merge($properties, $immediateProperties);
1205:
1206: $this->cachedProperties = $properties;
1207:
1208: return $this->cachedProperties;
1209: }
1210:
1211: /**
1212: * Get the property called $name.
1213: *
1214: * Returns null if property does not exist.
1215: *
1216: * @param non-empty-string $name
1217: */
1218: public function getProperty(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
1219: {
1220: $properties = $this->getProperties();
1221:
1222: if (! isset($properties[$name])) {
1223: return null;
1224: }
1225:
1226: return $properties[$name];
1227: }
1228:
1229: /**
1230: * Does this class have the specified property?
1231: *
1232: * @param non-empty-string $name
1233: */
1234: public function hasProperty(string $name): bool
1235: {
1236: return $this->getProperty($name) !== null;
1237: }
1238:
1239: /** @return array<non-empty-string, mixed> */
1240: public function getDefaultProperties(): array
1241: {
1242: return array_map(
1243: static fn (ReflectionProperty $property) => $property->getDefaultValue(),
1244: $this->getProperties(),
1245: );
1246: }
1247:
1248: /** @return non-empty-string|null */
1249: public function getFileName(): ?string
1250: {
1251: return $this->locatedSource->getFileName();
1252: }
1253:
1254: public function getLocatedSource(): LocatedSource
1255: {
1256: return $this->locatedSource;
1257: }
1258:
1259: /**
1260: * Get the line number that this class starts on.
1261: *
1262: * @return positive-int
1263: */
1264: public function getStartLine(): int
1265: {
1266: return $this->startLine;
1267: }
1268:
1269: /**
1270: * Get the line number that this class ends on.
1271: *
1272: * @return positive-int
1273: */
1274: public function getEndLine(): int
1275: {
1276: return $this->endLine;
1277: }
1278:
1279: /** @return positive-int */
1280: public function getStartColumn(): int
1281: {
1282: return $this->startColumn;
1283: }
1284:
1285: /** @return positive-int */
1286: public function getEndColumn(): int
1287: {
1288: return $this->endColumn;
1289: }
1290:
1291: /**
1292: * Get the parent class, if it is defined.
1293: */
1294: public function getParentClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
1295: {
1296: $parentClassName = $this->getParentClassName();
1297: if ($parentClassName === null) {
1298: return null;
1299: }
1300:
1301: if ($this->isOnParentClassCycle()) {
1302: return null;
1303: }
1304:
1305: try {
1306: return $this->reflector->reflectClass($parentClassName);
1307: } catch (IdentifierNotFound $exception) {
1308: return null;
1309: }
1310: }
1311:
1312: /**
1313: * Whether the class extends itself, directly or through other classes. PHP cannot declare such
1314: * a class, so it has no parent here, and every walk up the class hierarchy ends.
1315: */
1316: private function isOnParentClassCycle(): bool
1317: {
1318: return $this->cachedIsOnParentClassCycle ??= $this->reachesItself(
1319: static fn (self $class): array => $class->parentClassName === null ? [] : [$class->parentClassName],
1320: );
1321: }
1322:
1323: /**
1324: * Whether the interface extends itself, directly or through other interfaces. PHP cannot declare
1325: * such an interface, so it extends no interface here, and every walk over the hierarchy ends.
1326: */
1327: private function isOnInterfaceCycle(): bool
1328: {
1329: return $this->cachedIsOnInterfaceCycle ??= $this->reachesItself(
1330: static fn (self $class): array => $class->isInterface ? $class->implementsClassNames : [],
1331: );
1332: }
1333:
1334: /**
1335: * Whether the trait uses itself, directly or through other traits. PHP cannot declare such
1336: * a trait, so it uses no trait here, and every walk over the used traits ends.
1337: */
1338: private function isOnTraitCycle(): bool
1339: {
1340: return $this->cachedIsOnTraitCycle ??= $this->reachesItself(
1341: static fn (self $class): array => $class->isTrait ? $class->traitClassNames : [],
1342: );
1343: }
1344:
1345: /**
1346: * Follows the declared names, not the reflected classes, so that the answer depends only on
1347: * the declarations and not on which class is reflected first.
1348: *
1349: * @param callable(self): list<class-string> $edges
1350: */
1351: private function reachesItself(callable $edges): bool
1352: {
1353: $lowercasedName = strtolower($this->getName());
1354: $visited = [];
1355: $stack = $edges($this);
1356:
1357: while ($stack !== []) {
1358: $className = array_pop($stack);
1359: $lowercasedClassName = strtolower($className);
1360: if ($lowercasedClassName === $lowercasedName) {
1361: return true;
1362: }
1363:
1364: if (array_key_exists($lowercasedClassName, $visited)) {
1365: continue;
1366: }
1367:
1368: $visited[$lowercasedClassName] = true;
1369:
1370: try {
1371: $class = $this->reflector->reflectClass($className);
1372: } catch (IdentifierNotFound $exception) {
1373: continue;
1374: }
1375:
1376: foreach ($edges($class) as $nextClassName) {
1377: $stack[] = $nextClassName;
1378: }
1379: }
1380:
1381: return false;
1382: }
1383:
1384: /**
1385: * Gets the parent class names.
1386: *
1387: * @return list<class-string> A numerical array with parent class names as the values.
1388: */
1389: public function getParentClassNames(): array
1390: {
1391: return array_map(static fn (self $parentClass): string => $parentClass->getName(), $this->getParentClasses());
1392: }
1393:
1394: /** @return list<ReflectionClass> */
1395: private function getParentClasses(): array
1396: {
1397: if ($this->cachedParentClasses === null) {
1398: $parentClasses = [];
1399:
1400: // a class on a parent class cycle has no parent, so the chain always ends
1401: $class = $this;
1402: while (($parentClass = $class->getParentClass()) !== null) {
1403: $parentClasses[] = $parentClass;
1404: $class = $parentClass;
1405: }
1406:
1407: $this->cachedParentClasses = $parentClasses;
1408: }
1409:
1410: return $this->cachedParentClasses;
1411: }
1412:
1413: /** @return non-empty-string|null */
1414: public function getDocComment(): ?string
1415: {
1416: return $this->docComment;
1417: }
1418:
1419: public function isAnonymous(): bool
1420: {
1421: return $this->name === null;
1422: }
1423:
1424: /**
1425: * Is this an internal class?
1426: */
1427: public function isInternal(): bool
1428: {
1429: return $this->locatedSource->isInternal();
1430: }
1431:
1432: /**
1433: * Is this a user-defined function (will always return the opposite of
1434: * whatever isInternal returns).
1435: */
1436: public function isUserDefined(): bool
1437: {
1438: return ! $this->isInternal();
1439: }
1440:
1441: public function isDeprecated(): bool
1442: {
1443: return DeprecatedHelper::isDeprecated($this);
1444: }
1445:
1446: /**
1447: * Is this class an abstract class.
1448: */
1449: public function isAbstract(): bool
1450: {
1451: return (bool) ($this->modifiers & CoreReflectionClass::IS_EXPLICIT_ABSTRACT);
1452: }
1453:
1454: /**
1455: * Is this class a final class.
1456: */
1457: public function isFinal(): bool
1458: {
1459: if ($this->isEnum) {
1460: return true;
1461: }
1462:
1463: return (bool) ($this->modifiers & CoreReflectionClass::IS_FINAL);
1464: }
1465:
1466: public function isReadOnly(): bool
1467: {
1468: return (bool) ($this->modifiers & ReflectionClassAdapter::IS_READONLY_COMPATIBILITY);
1469: }
1470:
1471: /**
1472: * Get the core-reflection-compatible modifier values.
1473: *
1474: * @return int-mask-of<ReflectionClassAdapter::IS_*>
1475: */
1476: public function getModifiers(): int
1477: {
1478: return $this->modifiers;
1479: }
1480:
1481: /** @return int-mask-of<ReflectionClassAdapter::IS_*>
1482: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
1483: private function computeModifiers($node): int
1484: {
1485: if (! $node instanceof ClassNode) {
1486: return 0;
1487: }
1488:
1489: $modifiers = $node->isAbstract() ? CoreReflectionClass::IS_EXPLICIT_ABSTRACT : 0;
1490: $modifiers += $node->isFinal() ? CoreReflectionClass::IS_FINAL : 0;
1491: $modifiers += $node->isReadonly() ? ReflectionClassAdapter::IS_READONLY_COMPATIBILITY : 0;
1492:
1493: return $modifiers;
1494: }
1495:
1496: /**
1497: * Is this reflection a trait?
1498: */
1499: public function isTrait(): bool
1500: {
1501: return $this->isTrait;
1502: }
1503:
1504: /**
1505: * Is this reflection an interface?
1506: */
1507: public function isInterface(): bool
1508: {
1509: return $this->isInterface;
1510: }
1511:
1512: /**
1513: * Get the traits used, if any are defined. If this class does not have any
1514: * defined traits, this will return an empty array.
1515: *
1516: * @return list<ReflectionClass>
1517: */
1518: public function getTraits(): array
1519: {
1520: if ($this->cachedTraits !== null) {
1521: return $this->cachedTraits;
1522: }
1523:
1524: if ($this->isTrait && $this->isOnTraitCycle()) {
1525: return $this->cachedTraits = [];
1526: }
1527:
1528: $traits = [];
1529: foreach ($this->traitClassNames as $traitClassName) {
1530: try {
1531: $traits[] = $this->reflector->reflectClass($traitClassName);
1532: } catch (IdentifierNotFound $exception) {
1533: // pass
1534: }
1535: }
1536:
1537: return $this->cachedTraits = $traits;
1538: }
1539:
1540: /**
1541: * @param list<class-string> $interfaceClassNames
1542: *
1543: * @return list<class-string>
1544: */
1545: private function addStringableInterfaceClassName(array $interfaceClassNames): array
1546: {
1547: if (BetterReflection::$phpVersion < 80000) {
1548: return $interfaceClassNames;
1549: }
1550:
1551: /** @psalm-var class-string $stringableClassName */
1552: $stringableClassName = Stringable::class;
1553:
1554: if ($this->isInterface && $this->getName() === $stringableClassName) {
1555: return $interfaceClassNames;
1556: }
1557:
1558: if (in_array($stringableClassName, $interfaceClassNames, true)) {
1559: return $interfaceClassNames;
1560: }
1561:
1562: $methods = $this->immediateMethods;
1563: foreach ($this->getTraits() as $trait) {
1564: $methods += $trait->immediateMethods;
1565: }
1566:
1567: foreach (array_keys($methods) as $immediateMethodName) {
1568: if (strtolower($immediateMethodName) === '__tostring') {
1569: try {
1570: $interfaceClassNames[] = $stringableClassName;
1571: } catch (IdentifierNotFound $exception) {
1572: // Stringable interface does not exist on target PHP version
1573: }
1574:
1575: // @infection-ignore-all Break_: There's no difference between break and continue - break is just optimization
1576: break;
1577: }
1578: }
1579:
1580: return $interfaceClassNames;
1581: }
1582:
1583: /**
1584: * @param list<class-string> $interfaceClassNames
1585: *
1586: * @return list<class-string>
1587: */
1588: private function addEnumInterfaceClassNames(array $interfaceClassNames): array
1589: {
1590: assert($this->isEnum === true);
1591:
1592: $interfaceClassNames[] = UnitEnum::class;
1593:
1594: if ($this->isBackedEnum) {
1595: $interfaceClassNames[] = BackedEnum::class;
1596: }
1597:
1598: return $interfaceClassNames;
1599: }
1600:
1601: /** @return list<trait-string> */
1602: public function getTraitClassNames(): array
1603: {
1604: return $this->traitClassNames;
1605: }
1606:
1607: /**
1608: * Get the names of the traits used as an array of strings, if any are
1609: * defined. If this class does not have any defined traits, this will
1610: * return an empty array.
1611: *
1612: * @return list<trait-string>
1613: */
1614: public function getTraitNames(): array
1615: {
1616: return array_map(
1617: static function (ReflectionClass $trait): string {
1618: /** @psalm-var trait-string $traitName */
1619: $traitName = $trait->getName();
1620:
1621: return $traitName;
1622: },
1623: $this->getTraits(),
1624: );
1625: }
1626:
1627: /**
1628: * Return a list of the aliases used when importing traits for this class.
1629: * The returned array is in key/value pair in this format:.
1630: *
1631: * 'aliasedMethodName' => 'ActualClass::actualMethod'
1632: *
1633: * @return array<non-empty-string, non-empty-string>
1634: *
1635: * @example
1636: * // When reflecting a class such as:
1637: * class Foo
1638: * {
1639: * use MyTrait {
1640: * myTraitMethod as myAliasedMethod;
1641: * }
1642: * }
1643: * // This method would return
1644: * // ['myAliasedMethod' => 'MyTrait::myTraitMethod']
1645: */
1646: public function getTraitAliases(): array
1647: {
1648: if ($this->traitsData['aliases'] === []) {
1649: return [];
1650: }
1651:
1652: $traits = [];
1653: foreach ($this->getTraits() as $trait) {
1654: $traits[$trait->getName()] = $trait;
1655: }
1656: $traitAliases = [];
1657:
1658: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
1659: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
1660: if (!array_key_exists($traitClassName, $traits)) {
1661: continue;
1662: }
1663: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
1664: continue;
1665: }
1666:
1667: $traitAliases[$traitAliasDefinition['alias']] = $this->traitsData['hashes'][$traitAliasDefinition['hash']];
1668: }
1669: }
1670:
1671: return $traitAliases;
1672: }
1673:
1674: /**
1675: * Returns data when importing traits for this class:
1676: *
1677: * 'aliases': List of the aliases used when importing traits. In format:
1678: *
1679: * 'traitClassName' => ['alias' => 'aliasedMethodName', 'method' => 'actualMethodName', 'hash' => 'traitClassName::actualMethodName'],
1680: *
1681: * Example:
1682: * // When reflecting a code such as:
1683: *
1684: * use MyTrait {
1685: * myTraitMethod as myAliasedMethod;
1686: * }
1687: *
1688: * // This method would return
1689: * // ['MyTrait' => ['alias' => 'myAliasedMethod', 'method' => 'myTraitMethod', 'hash' => 'mytrait::mytraitmethod']]
1690: *
1691: * 'modifiers': Used modifiers when importing traits. In format:
1692: *
1693: * 'methodName' => 'modifier'
1694: *
1695: * Example:
1696: * // When reflecting a code such as:
1697: *
1698: * use MyTrait {
1699: * myTraitMethod as public;
1700: * }
1701: *
1702: * // This method would return
1703: * // ['myTraitMethod' => 1]
1704: *
1705: * 'precedences': Precedences used when importing traits. In format:
1706: *
1707: * 'Class::method' => 'Class::method'
1708: *
1709: * Example:
1710: * // When reflecting a code such as:
1711: *
1712: * use MyTrait, MyTrait2 {
1713: * MyTrait2::foo insteadof MyTrait1;
1714: * }
1715: *
1716: * // This method would return
1717: * // ['MyTrait1::foo' => 'MyTrait2::foo']
1718: *
1719: * @return array{
1720: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1721: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1722: * precedences: array<non-empty-string, non-empty-string>,
1723: * hashes: array<non-empty-string, non-empty-string>,
1724: * }
1725: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
1726: */
1727: private function computeTraitsData($node): array
1728: {
1729: $traitsData = [
1730: 'aliases' => [],
1731: 'modifiers' => [],
1732: 'precedences' => [],
1733: 'hashes' => [],
1734: ];
1735:
1736: foreach ($node->getTraitUses() as $traitUsage) {
1737: foreach ($traitUsage->adaptations as $adaptation) {
1738: $usedTraits = $adaptation->trait !== null ? [$adaptation->trait] : $traitUsage->traits;
1739: $traitsData = $this->processTraitAdaptation($adaptation, $usedTraits, $traitsData);
1740: }
1741: }
1742:
1743: return $traitsData;
1744: }
1745:
1746: /**
1747: * @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
1748: *
1749: * @param array<array-key, Node\Name> $usedTraits
1750: * @param array{
1751: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1752: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1753: * precedences: array<non-empty-string, non-empty-string>,
1754: * hashes: array<non-empty-string, non-empty-string>,
1755: * } $traitsData
1756: *
1757: * @return array{
1758: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1759: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1760: * precedences: array<non-empty-string, non-empty-string>,
1761: * hashes: array<non-empty-string, non-empty-string>,
1762: * }
1763: */
1764: private function processTraitAdaptation(Node\Stmt\TraitUseAdaptation $adaptation, array $usedTraits, array $traitsData): array
1765: {
1766: foreach ($usedTraits as $usedTrait) {
1767: $methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1768: $lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
1769:
1770: $traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
1771:
1772: if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1773: if ($adaptation->newModifier !== null) {
1774: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1775: $modifier = $adaptation->newModifier;
1776: $traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
1777: }
1778:
1779: if ($adaptation->newName !== null) {
1780: // We need to save all possible combinations of trait and method names
1781: // The real aliases will be filtered in getters
1782: /** @var trait-string $usedTraitClassName */
1783: $usedTraitClassName = $usedTrait->toString();
1784: $traitsData['aliases'][$usedTraitClassName][] = [
1785: 'alias' => $adaptation->newName->name,
1786: 'method' => $adaptation->method->toString(),
1787: 'hash' => $lowerCasedMethodHash,
1788: ];
1789: }
1790: } elseif ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) {
1791: foreach ($adaptation->insteadof as $insteadof) {
1792: $adaptationNameHash = $this->lowerCasedMethodHash($insteadof->toString(), $adaptation->method->toString());
1793:
1794: $traitsData['precedences'][$adaptationNameHash] = $lowerCasedMethodHash;
1795: }
1796: }
1797: }
1798:
1799: return $traitsData;
1800: }
1801:
1802: /**
1803: * @return non-empty-string
1804: *
1805: * @psalm-pure
1806: */
1807: private function methodHash(string $className, string $methodName): string
1808: {
1809: return sprintf(
1810: '%s::%s',
1811: $className,
1812: $methodName,
1813: );
1814: }
1815:
1816: /** @return non-empty-string */
1817: private function lowerCasedMethodHash(string $className, string $methodName): string
1818: {
1819: return strtolower($this->methodHash($className, $methodName));
1820: }
1821:
1822: /** @return list<class-string> */
1823: public function getInterfaceClassNames(): array
1824: {
1825: $implementsClassNames = $this->implementsClassNames;
1826:
1827: if ($this->isEnum) {
1828: $implementsClassNames = $this->addEnumInterfaceClassNames($implementsClassNames);
1829: }
1830:
1831: return $this->addStringableInterfaceClassName($implementsClassNames);
1832: }
1833:
1834: /**
1835: * Gets the interfaces.
1836: *
1837: * @link https://php.net/manual/en/reflectionclass.getinterfaces.php
1838: *
1839: * @return array<class-string, self> An associative array of interfaces, with keys as interface names and the array
1840: * values as {@see ReflectionClass} objects.
1841: */
1842: public function getInterfaces(): array
1843: {
1844: if ($this->cachedInterfaces !== null) {
1845: return $this->cachedInterfaces;
1846: }
1847:
1848: $interfaces = array_merge(
1849: [$this->getCurrentClassImplementedInterfacesIndexedByName()],
1850: array_map(
1851: static fn (self $parentClass): array => $parentClass->getCurrentClassImplementedInterfacesIndexedByName(),
1852: $this->getParentClasses(),
1853: ),
1854: );
1855:
1856: return $this->cachedInterfaces = array_merge(...array_reverse($interfaces));
1857: }
1858:
1859: /**
1860: * Get only the interfaces that this class implements (i.e. do not search
1861: * up parent classes etc.)
1862: *
1863: * @return array<class-string, self>
1864: */
1865: public function getImmediateInterfaces(): array
1866: {
1867: if ($this->isTrait) {
1868: return [];
1869: }
1870:
1871: if ($this->isInterface && $this->isOnInterfaceCycle()) {
1872: return [];
1873: }
1874:
1875: $interfaces = [];
1876: foreach ($this->getInterfaceClassNames() as $interfaceClassName) {
1877: try {
1878: $interfaces[$interfaceClassName] = $this->reflector->reflectClass($interfaceClassName);
1879: } catch (IdentifierNotFound $exception) {
1880: continue;
1881: }
1882: }
1883:
1884: return $interfaces;
1885: }
1886:
1887: /**
1888: * Gets the interface names.
1889: *
1890: * @link https://php.net/manual/en/reflectionclass.getinterfacenames.php
1891: *
1892: * @return list<class-string> A numerical array with interface names as the values.
1893: */
1894: public function getInterfaceNames(): array
1895: {
1896: if ($this->cachedInterfaceNames !== null) {
1897: return $this->cachedInterfaceNames;
1898: }
1899:
1900: return $this->cachedInterfaceNames = array_values(array_map(
1901: static fn (self $interface): string => $interface->getName(),
1902: $this->getInterfaces(),
1903: ));
1904: }
1905:
1906: /**
1907: * Checks whether the given object is an instance.
1908: *
1909: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1910: */
1911: public function isInstance(object $object): bool
1912: {
1913: $className = $this->getName();
1914:
1915: // note: since $object was loaded, we can safely assume that $className is available in the current
1916: // php script execution context
1917: return $object instanceof $className;
1918: }
1919:
1920: /**
1921: * Checks whether the given class string is a subclass of this class.
1922: *
1923: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1924: */
1925: public function isSubclassOf(string $className): bool
1926: {
1927: return in_array(
1928: ltrim($className, '\\'),
1929: $this->getParentClassNames(),
1930: true,
1931: );
1932: }
1933:
1934: /**
1935: * Checks whether this class implements the given interface.
1936: *
1937: * @link https://php.net/manual/en/reflectionclass.implementsinterface.php
1938: */
1939: public function implementsInterface(string $interfaceName): bool
1940: {
1941: return in_array(ltrim($interfaceName, '\\'), $this->getInterfaceNames(), true);
1942: }
1943:
1944: /**
1945: * Checks whether this reflection is an instantiable class
1946: *
1947: * @link https://php.net/manual/en/reflectionclass.isinstantiable.php
1948: */
1949: public function isInstantiable(): bool
1950: {
1951: // @TODO doesn't consider internal non-instantiable classes yet.
1952:
1953: if ($this->isAbstract()) {
1954: return false;
1955: }
1956:
1957: if ($this->isInterface()) {
1958: return false;
1959: }
1960:
1961: if ($this->isTrait()) {
1962: return false;
1963: }
1964:
1965: $constructor = $this->getConstructor();
1966:
1967: if ($constructor === null) {
1968: return true;
1969: }
1970:
1971: return $constructor->isPublic();
1972: }
1973:
1974: /**
1975: * Checks whether this is a reflection of a class that supports the clone operator
1976: *
1977: * @link https://php.net/manual/en/reflectionclass.iscloneable.php
1978: */
1979: public function isCloneable(): bool
1980: {
1981: if (! $this->isInstantiable()) {
1982: return false;
1983: }
1984:
1985: $cloneMethod = $this->getMethod('__clone');
1986:
1987: if ($cloneMethod === null) {
1988: return true;
1989: }
1990:
1991: return $cloneMethod->isPublic();
1992: }
1993:
1994: /**
1995: * Checks if iterateable
1996: *
1997: * @link https://php.net/manual/en/reflectionclass.isiterateable.php
1998: */
1999: public function isIterateable(): bool
2000: {
2001: return $this->isInstantiable() && $this->implementsInterface(Traversable::class);
2002: }
2003:
2004: public function isEnum(): bool
2005: {
2006: return $this->isEnum;
2007: }
2008:
2009: /** @return array<class-string, ReflectionClass> */
2010: private function getCurrentClassImplementedInterfacesIndexedByName(): array
2011: {
2012: if ($this->isTrait) {
2013: return [];
2014: }
2015:
2016: if ($this->isInterface) {
2017: // assumption: first key is the current interface
2018: return array_slice($this->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()), 1);
2019: }
2020:
2021: $interfaces = [];
2022: foreach ($this->getInterfaceClassNames() as $name) {
2023: try {
2024: $interface = $this->reflector->reflectClass($name);
2025: foreach ($interface->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()) as $n => $i) {
2026: $interfaces[$n] = $i;
2027: }
2028: } catch (IdentifierNotFound $exception) {
2029: continue;
2030: }
2031: }
2032:
2033: return $interfaces;
2034: }
2035:
2036: /**
2037: * This method allows us to retrieve all interfaces parent of this interface. Do not use on class nodes!
2038: *
2039: * @return array<class-string, ReflectionClass> parent interfaces of this interface
2040: */
2041: private function getInterfacesHierarchy(AlreadyVisitedClasses $alreadyVisitedClasses): array
2042: {
2043: if (! $this->isInterface) {
2044: return [];
2045: }
2046:
2047: $interfaceClassName = $this->getName();
2048: $alreadyVisitedClasses->push($interfaceClassName);
2049:
2050: /** @var array<class-string, self> $interfaces */
2051: $interfaces = [$interfaceClassName => $this];
2052: foreach ($this->getImmediateInterfaces() as $interface) {
2053: $alreadyVisitedClassesCopyForInterface = clone $alreadyVisitedClasses;
2054: foreach ($interface->getInterfacesHierarchy($alreadyVisitedClassesCopyForInterface) as $extendedInterfaceName => $extendedInterface) {
2055: $interfaces[$extendedInterfaceName] = $extendedInterface;
2056: }
2057: }
2058:
2059: return $interfaces;
2060: }
2061:
2062: /**
2063: * Get the value of a static property, if it exists. Throws a
2064: * PropertyDoesNotExist exception if it does not exist or is not static.
2065: * (note, differs very slightly from internal reflection behaviour)
2066: *
2067: * @param non-empty-string $propertyName
2068: *
2069: * @throws ClassDoesNotExist
2070: * @throws NoObjectProvided
2071: * @throws NotAnObject
2072: * @throws ObjectNotInstanceOfClass
2073: * @return mixed
2074: */
2075: public function getStaticPropertyValue(string $propertyName)
2076: {
2077: $property = $this->getProperty($propertyName);
2078:
2079: if (! $property || ! $property->isStatic()) {
2080: throw PropertyDoesNotExist::fromName($propertyName);
2081: }
2082:
2083: return $property->getValue();
2084: }
2085:
2086: /**
2087: * Set the value of a static property
2088: *
2089: * @param non-empty-string $propertyName
2090: *
2091: * @throws ClassDoesNotExist
2092: * @throws NoObjectProvided
2093: * @throws NotAnObject
2094: * @throws ObjectNotInstanceOfClass
2095: * @param mixed $value
2096: */
2097: public function setStaticPropertyValue(string $propertyName, $value): void
2098: {
2099: $property = $this->getProperty($propertyName);
2100:
2101: if (! $property || ! $property->isStatic()) {
2102: throw PropertyDoesNotExist::fromName($propertyName);
2103: }
2104:
2105: $property->setValue($value);
2106: }
2107:
2108: /** @return array<non-empty-string, mixed> */
2109: public function getStaticProperties(): array
2110: {
2111: $staticProperties = [];
2112:
2113: foreach ($this->getProperties() as $property) {
2114: if (! $property->isStatic()) {
2115: continue;
2116: }
2117:
2118: /** @psalm-suppress MixedAssignment */
2119: $staticProperties[$property->getName()] = $property->getValue();
2120: }
2121:
2122: return $staticProperties;
2123: }
2124:
2125: /** @return list<ReflectionAttribute> */
2126: public function getAttributes(): array
2127: {
2128: return $this->attributes;
2129: }
2130:
2131: /** @return list<ReflectionAttribute> */
2132: public function getAttributesByName(string $name): array
2133: {
2134: if ($this->attributes === []) {
2135: return [];
2136: }
2137:
2138: return ReflectionAttributeHelper::filterAttributesByName($this->attributes, $name);
2139: }
2140:
2141: /**
2142: * @param class-string $className
2143: *
2144: * @return list<ReflectionAttribute>
2145: */
2146: public function getAttributesByInstance(string $className): array
2147: {
2148: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
2149: }
2150: }
2151: