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