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