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