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