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