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