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->cachedMethods ?? $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: $methods = $this->cachedMethods ?? $this->getMethodsIndexedByLowercasedName(AlreadyVisitedClasses::createEmpty());
827:
828: return $methods[strtolower($methodName)] ?? null;
829: }
830:
831: /**
832: * Does the class have the specified method?
833: *
834: * @param non-empty-string $methodName
835: */
836: public function hasMethod(string $methodName): bool
837: {
838: return $this->getMethod($methodName) !== null;
839: }
840:
841: /**
842: * Get an associative array of only the constants for this specific class (i.e. do not search
843: * up parent classes etc.), with keys as constant names and values as {@see ReflectionClassConstant} objects.
844: *
845: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
846: *
847: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
848: */
849: public function getImmediateConstants(int $filter = 0): array
850: {
851: if ($filter === 0) {
852: return $this->immediateConstants;
853: }
854:
855: return array_filter(
856: $this->immediateConstants,
857: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
858: );
859: }
860:
861: /**
862: * Does this class have the specified constant?
863: *
864: * @param non-empty-string $name
865: */
866: public function hasConstant(string $name): bool
867: {
868: return $this->getConstant($name) !== null;
869: }
870:
871: /**
872: * Get the reflection object of the specified class constant.
873: *
874: * Returns null if not specified.
875: *
876: * @param non-empty-string $name
877: */
878: public function getConstant(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionClassConstant
879: {
880: return $this->getConstants()[$name] ?? null;
881: }
882:
883: /** @return array<non-empty-string, ReflectionClassConstant>
884: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
885: private function createImmediateConstants($node, Reflector $reflector): array
886: {
887: $constants = [];
888:
889: foreach ($node->getConstants() as $constantsNode) {
890: foreach (array_keys($constantsNode->consts) as $constantPositionInNode) {
891: assert(is_int($constantPositionInNode));
892: $constant = ReflectionClassConstant::createFromNode($reflector, $constantsNode, $constantPositionInNode, $this, $this);
893:
894: $constants[$constant->getName()] = $constant;
895: }
896: }
897:
898: return $constants;
899: }
900:
901: /**
902: * Get an associative array of the defined constants in this class,
903: * with keys as constant names and values as {@see ReflectionClassConstant} objects.
904: *
905: * @param int-mask-of<ReflectionClassConstantAdapter::IS_*> $filter
906: *
907: * @return array<non-empty-string, ReflectionClassConstant> indexed by name
908: */
909: public function getConstants(int $filter = 0): array
910: {
911: $constants = $this->cachedConstants ?? $this->getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
912:
913: if ($filter === 0) {
914: return $constants;
915: }
916:
917: return array_filter(
918: $constants,
919: static fn (ReflectionClassConstant $constant): bool => (bool) ($filter & $constant->getModifiers()),
920: );
921: }
922:
923: /** @return array<non-empty-string, ReflectionClassConstant> indexed by name */
924: private function getConstantsConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
925: {
926: if ($this->cachedConstants !== null) {
927: return $this->cachedConstants;
928: }
929:
930: $alreadyVisitedClasses->push($this->getName());
931:
932: // Note: constants are not merged via their name as array index, since internal PHP constant
933: // sorting does not follow `\array_merge()` semantics
934:
935: $constants = $this->getImmediateConstants();
936:
937: $parentClass = $this->getParentClass();
938: if ($parentClass !== null) {
939: foreach ($parentClass->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
940: if ($constant->isPrivate()) {
941: continue;
942: }
943:
944: if (array_key_exists($constantName, $constants)) {
945: continue;
946: }
947:
948: $constants[$constantName] = $constant;
949: }
950: }
951:
952: foreach ($this->getTraits() as $trait) {
953: foreach ($trait->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $constantName => $constant) {
954: if (array_key_exists($constantName, $constants)) {
955: continue;
956: }
957:
958: $constants[$constantName] = $constant->withImplementingClass($this);
959: }
960: }
961:
962: foreach ($this->getImmediateInterfaces() as $interface) {
963: $alreadyVisitedClassesCopy = clone $alreadyVisitedClasses;
964: foreach ($interface->getConstantsConsideringAlreadyVisitedClasses($alreadyVisitedClassesCopy) as $constantName => $constant) {
965: if (array_key_exists($constantName, $constants)) {
966: continue;
967: }
968:
969: $constants[$constantName] = $constant;
970: }
971: }
972:
973: $this->cachedConstants = $constants;
974:
975: return $this->cachedConstants;
976: }
977:
978: /**
979: * Get the constructor method for this class.
980: */
981: public function getConstructor(): ?\PHPStan\BetterReflection\Reflection\ReflectionMethod
982: {
983: if ($this->cachedConstructor !== null) {
984: return $this->cachedConstructor;
985: }
986:
987: $constructors = array_values(array_filter($this->getMethods(), static fn (ReflectionMethod $method): bool => $method->isConstructor()));
988:
989: return $this->cachedConstructor = $constructors[0] ?? null;
990: }
991:
992: /**
993: * Get only the properties for this specific class (i.e. do not search
994: * up parent classes etc.)
995: *
996: * @see ReflectionClass::getProperties() for the usage of filter
997: *
998: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
999: *
1000: * @return array<non-empty-string, ReflectionProperty>
1001: */
1002: public function getImmediateProperties(int $filter = 0): array
1003: {
1004: if ($filter === 0) {
1005: return $this->immediateProperties;
1006: }
1007:
1008: return array_filter(
1009: $this->immediateProperties,
1010: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
1011: );
1012: }
1013:
1014: /** @return array<non-empty-string, ReflectionProperty>
1015: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
1016: private function createImmediateProperties($node, Reflector $reflector): array
1017: {
1018: $properties = [];
1019:
1020: foreach ($node->getProperties() as $propertiesNode) {
1021: foreach ($propertiesNode->props as $propertyPropertyNode) {
1022: $property = ReflectionProperty::createFromNode(
1023: $reflector,
1024: $propertiesNode,
1025: $propertyPropertyNode,
1026: $this,
1027: $this,
1028: );
1029: $properties[$property->getName()] = $property;
1030: }
1031: }
1032:
1033: foreach ($node->getMethods() as $methodNode) {
1034: if ($methodNode->name->toLowerString() !== '__construct') {
1035: continue;
1036: }
1037:
1038: foreach ($methodNode->params as $parameterNode) {
1039: if ($parameterNode->flags === 0 && $parameterNode->hooks === []) {
1040: // No flags, no promotion
1041: continue;
1042: }
1043:
1044: $parameterNameNode = $parameterNode->var;
1045: assert($parameterNameNode instanceof Node\Expr\Variable);
1046: assert(is_string($parameterNameNode->name));
1047:
1048: $propertyNode = new Node\Stmt\Property(
1049: $parameterNode->flags,
1050: [new Node\PropertyItem($parameterNameNode->name, $parameterNode->default)],
1051: $parameterNode->getAttributes(),
1052: $parameterNode->type,
1053: $parameterNode->attrGroups,
1054: $parameterNode->hooks,
1055: );
1056: $property = ReflectionProperty::createFromNode(
1057: $reflector,
1058: $propertyNode,
1059: $propertyNode->props[0],
1060: $this,
1061: $this,
1062: true,
1063: );
1064: $properties[$property->getName()] = $property;
1065: }
1066: }
1067:
1068: if ($node instanceof EnumNode || $node instanceof InterfaceNode) {
1069: $properties = $this->addEnumProperties($properties, $node, $reflector);
1070: }
1071:
1072: return $properties;
1073: }
1074:
1075: /**
1076: * @param array<non-empty-string, ReflectionProperty> $properties
1077: *
1078: * @return array<non-empty-string, ReflectionProperty>
1079: * @param EnumNode|InterfaceNode $node
1080: */
1081: private function addEnumProperties(array $properties, $node, Reflector $reflector): array
1082: {
1083: $createProperty = function (string $name, $type) use ($reflector): ReflectionProperty {
1084: $propertyNode = new Node\Stmt\Property(
1085: Modifiers::PUBLIC | Modifiers::READONLY,
1086: [new Node\PropertyItem($name)],
1087: [],
1088: $type,
1089: );
1090:
1091: return ReflectionProperty::createFromNode(
1092: $reflector,
1093: $propertyNode,
1094: $propertyNode->props[0],
1095: $this,
1096: $this,
1097: );
1098: };
1099:
1100: if ($node instanceof InterfaceNode) {
1101: $interfaceName = $this->getName();
1102: if ($interfaceName === 'UnitEnum') {
1103: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
1104: }
1105:
1106: if ($interfaceName === 'BackedEnum') {
1107: $properties['value'] = $createProperty('value', new Node\UnionType([
1108: new Node\Identifier('int'),
1109: new Node\Identifier('string'),
1110: ]));
1111: }
1112: } else {
1113: $properties['name'] = $createProperty('name', new Node\Identifier('string'));
1114:
1115: if ($node->scalarType !== null) {
1116: $properties['value'] = $createProperty('value', $node->scalarType);
1117: }
1118: }
1119:
1120: return $properties;
1121: }
1122:
1123: /**
1124: * Get the properties for this class.
1125: *
1126: * Filter the results to include only properties with certain attributes. Defaults
1127: * to no filtering.
1128: * Any combination of \ReflectionProperty::IS_STATIC,
1129: * \ReflectionProperty::IS_PUBLIC,
1130: * \ReflectionProperty::IS_PROTECTED,
1131: * \ReflectionProperty::IS_PRIVATE.
1132: * For example if $filter = \ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC
1133: * only the static public properties will be returned
1134: *
1135: * @param int-mask-of<ReflectionPropertyAdapter::IS_*> $filter
1136: *
1137: * @return array<non-empty-string, ReflectionProperty>
1138: */
1139: public function getProperties(int $filter = 0): array
1140: {
1141: $properties = $this->cachedProperties ?? $this->getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses::createEmpty());
1142:
1143: if ($filter === 0) {
1144: return $properties;
1145: }
1146:
1147: return array_filter(
1148: $properties,
1149: static fn (ReflectionProperty $property): bool => (bool) ($filter & $property->getModifiers()),
1150: );
1151: }
1152:
1153: /** @return array<non-empty-string, ReflectionProperty> */
1154: private function getPropertiesConsideringAlreadyVisitedClasses(AlreadyVisitedClasses $alreadyVisitedClasses): array
1155: {
1156: if ($this->cachedProperties !== null) {
1157: return $this->cachedProperties;
1158: }
1159:
1160: $alreadyVisitedClasses->push($this->getName());
1161:
1162: $immediateProperties = $this->getImmediateProperties();
1163:
1164: // Merging together properties from parent class, interfaces, traits, current class (in this precise order)
1165:
1166: $properties = array_merge(
1167: array_filter(
1168: (($nullsafeVariable2 = $this->getParentClass()) ? $nullsafeVariable2->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) : null) ?? [],
1169: static fn (ReflectionProperty $property) => ! $property->isPrivate(),
1170: ),
1171: ...array_map(
1172: static fn (ReflectionClass $ancestor): array => $ancestor->getPropertiesConsideringAlreadyVisitedClasses(clone $alreadyVisitedClasses),
1173: array_values($this->getImmediateInterfaces()),
1174: ),
1175: );
1176:
1177: foreach ($this->getTraits() as $trait) {
1178: foreach ($trait->getPropertiesConsideringAlreadyVisitedClasses($alreadyVisitedClasses) as $traitProperty) {
1179: $traitPropertyName = $traitProperty->getName();
1180:
1181: $existingProperty = $immediateProperties[$traitPropertyName] ?? $properties[$traitPropertyName] ?? null;
1182:
1183: if ($existingProperty !== null && ! $existingProperty->isAbstract()) {
1184: continue;
1185: }
1186:
1187: $properties[$traitPropertyName] = $traitProperty->withImplementingClass($this);
1188: }
1189: }
1190:
1191: // Merge immediate properties last to get the required order
1192: $properties = array_merge($properties, $immediateProperties);
1193:
1194: $this->cachedProperties = $properties;
1195:
1196: return $this->cachedProperties;
1197: }
1198:
1199: /**
1200: * Get the property called $name.
1201: *
1202: * Returns null if property does not exist.
1203: *
1204: * @param non-empty-string $name
1205: */
1206: public function getProperty(string $name): ?\PHPStan\BetterReflection\Reflection\ReflectionProperty
1207: {
1208: $properties = $this->getProperties();
1209:
1210: if (! isset($properties[$name])) {
1211: return null;
1212: }
1213:
1214: return $properties[$name];
1215: }
1216:
1217: /**
1218: * Does this class have the specified property?
1219: *
1220: * @param non-empty-string $name
1221: */
1222: public function hasProperty(string $name): bool
1223: {
1224: return $this->getProperty($name) !== null;
1225: }
1226:
1227: /** @return array<non-empty-string, mixed> */
1228: public function getDefaultProperties(): array
1229: {
1230: return array_map(
1231: static fn (ReflectionProperty $property) => $property->getDefaultValue(),
1232: $this->getProperties(),
1233: );
1234: }
1235:
1236: /** @return non-empty-string|null */
1237: public function getFileName(): ?string
1238: {
1239: return $this->locatedSource->getFileName();
1240: }
1241:
1242: public function getLocatedSource(): LocatedSource
1243: {
1244: return $this->locatedSource;
1245: }
1246:
1247: /**
1248: * Get the line number that this class starts on.
1249: *
1250: * @return positive-int
1251: */
1252: public function getStartLine(): int
1253: {
1254: return $this->startLine;
1255: }
1256:
1257: /**
1258: * Get the line number that this class ends on.
1259: *
1260: * @return positive-int
1261: */
1262: public function getEndLine(): int
1263: {
1264: return $this->endLine;
1265: }
1266:
1267: /** @return positive-int */
1268: public function getStartColumn(): int
1269: {
1270: return $this->startColumn;
1271: }
1272:
1273: /** @return positive-int */
1274: public function getEndColumn(): int
1275: {
1276: return $this->endColumn;
1277: }
1278:
1279: /**
1280: * Get the parent class, if it is defined.
1281: */
1282: public function getParentClass(): ?\PHPStan\BetterReflection\Reflection\ReflectionClass
1283: {
1284: $parentClassName = $this->getParentClassName();
1285: if ($parentClassName === null) {
1286: return null;
1287: }
1288:
1289: if ($this->name === $parentClassName) {
1290: throw CircularReference::fromClassName($parentClassName);
1291: }
1292:
1293: try {
1294: return $this->reflector->reflectClass($parentClassName);
1295: } catch (IdentifierNotFound $exception) {
1296: return null;
1297: }
1298: }
1299:
1300: /**
1301: * Gets the parent class names.
1302: *
1303: * @return list<class-string> A numerical array with parent class names as the values.
1304: */
1305: public function getParentClassNames(): array
1306: {
1307: return array_map(static fn (self $parentClass): string => $parentClass->getName(), $this->getParentClasses());
1308: }
1309:
1310: /** @return list<ReflectionClass> */
1311: private function getParentClasses(): array
1312: {
1313: if ($this->cachedParentClasses === null) {
1314: $parentClasses = [];
1315:
1316: $parentClassName = $this->parentClassName;
1317: while ($parentClassName !== null) {
1318: try {
1319: $parentClass = $this->reflector->reflectClass($parentClassName);
1320: } catch (IdentifierNotFound $exception) {
1321: break;
1322: }
1323:
1324: if (
1325: $this->name === $parentClassName
1326: || array_key_exists($parentClassName, $parentClasses)
1327: ) {
1328: throw CircularReference::fromClassName($parentClassName);
1329: }
1330:
1331: $parentClasses[$parentClassName] = $parentClass;
1332:
1333: $parentClassName = $parentClass->parentClassName;
1334: }
1335:
1336: $this->cachedParentClasses = array_values($parentClasses);
1337: }
1338:
1339: return $this->cachedParentClasses;
1340: }
1341:
1342: /** @return non-empty-string|null */
1343: public function getDocComment(): ?string
1344: {
1345: return $this->docComment;
1346: }
1347:
1348: public function isAnonymous(): bool
1349: {
1350: return $this->name === null;
1351: }
1352:
1353: /**
1354: * Is this an internal class?
1355: */
1356: public function isInternal(): bool
1357: {
1358: return $this->locatedSource->isInternal();
1359: }
1360:
1361: /**
1362: * Is this a user-defined function (will always return the opposite of
1363: * whatever isInternal returns).
1364: */
1365: public function isUserDefined(): bool
1366: {
1367: return ! $this->isInternal();
1368: }
1369:
1370: public function isDeprecated(): bool
1371: {
1372: return DeprecatedHelper::isDeprecated($this);
1373: }
1374:
1375: /**
1376: * Is this class an abstract class.
1377: */
1378: public function isAbstract(): bool
1379: {
1380: return (bool) ($this->modifiers & CoreReflectionClass::IS_EXPLICIT_ABSTRACT);
1381: }
1382:
1383: /**
1384: * Is this class a final class.
1385: */
1386: public function isFinal(): bool
1387: {
1388: if ($this->isEnum) {
1389: return true;
1390: }
1391:
1392: return (bool) ($this->modifiers & CoreReflectionClass::IS_FINAL);
1393: }
1394:
1395: public function isReadOnly(): bool
1396: {
1397: return (bool) ($this->modifiers & ReflectionClassAdapter::IS_READONLY_COMPATIBILITY);
1398: }
1399:
1400: /**
1401: * Get the core-reflection-compatible modifier values.
1402: *
1403: * @return int-mask-of<ReflectionClassAdapter::IS_*>
1404: */
1405: public function getModifiers(): int
1406: {
1407: return $this->modifiers;
1408: }
1409:
1410: /** @return int-mask-of<ReflectionClassAdapter::IS_*>
1411: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node */
1412: private function computeModifiers($node): int
1413: {
1414: if (! $node instanceof ClassNode) {
1415: return 0;
1416: }
1417:
1418: $modifiers = $node->isAbstract() ? CoreReflectionClass::IS_EXPLICIT_ABSTRACT : 0;
1419: $modifiers += $node->isFinal() ? CoreReflectionClass::IS_FINAL : 0;
1420: $modifiers += $node->isReadonly() ? ReflectionClassAdapter::IS_READONLY_COMPATIBILITY : 0;
1421:
1422: return $modifiers;
1423: }
1424:
1425: /**
1426: * Is this reflection a trait?
1427: */
1428: public function isTrait(): bool
1429: {
1430: return $this->isTrait;
1431: }
1432:
1433: /**
1434: * Is this reflection an interface?
1435: */
1436: public function isInterface(): bool
1437: {
1438: return $this->isInterface;
1439: }
1440:
1441: /**
1442: * Get the traits used, if any are defined. If this class does not have any
1443: * defined traits, this will return an empty array.
1444: *
1445: * @return list<ReflectionClass>
1446: */
1447: public function getTraits(): array
1448: {
1449: if ($this->cachedTraits !== null) {
1450: return $this->cachedTraits;
1451: }
1452:
1453: $traits = [];
1454: foreach ($this->traitClassNames as $traitClassName) {
1455: try {
1456: $traits[] = $this->reflector->reflectClass($traitClassName);
1457: } catch (IdentifierNotFound $exception) {
1458: // pass
1459: }
1460: }
1461:
1462: return $this->cachedTraits = $traits;
1463: }
1464:
1465: /**
1466: * @param list<class-string> $interfaceClassNames
1467: *
1468: * @return list<class-string>
1469: */
1470: private function addStringableInterfaceClassName(array $interfaceClassNames): array
1471: {
1472: if (BetterReflection::$phpVersion < 80000) {
1473: return $interfaceClassNames;
1474: }
1475:
1476: /** @psalm-var class-string $stringableClassName */
1477: $stringableClassName = Stringable::class;
1478:
1479: if ($this->isInterface && $this->getName() === $stringableClassName) {
1480: return $interfaceClassNames;
1481: }
1482:
1483: if (in_array($stringableClassName, $interfaceClassNames, true)) {
1484: return $interfaceClassNames;
1485: }
1486:
1487: $methods = $this->immediateMethods;
1488: foreach ($this->getTraits() as $trait) {
1489: $methods += $trait->immediateMethods;
1490: }
1491:
1492: foreach (array_keys($methods) as $immediateMethodName) {
1493: if (strtolower($immediateMethodName) === '__tostring') {
1494: try {
1495: $interfaceClassNames[] = $stringableClassName;
1496: } catch (IdentifierNotFound $exception) {
1497: // Stringable interface does not exist on target PHP version
1498: }
1499:
1500: // @infection-ignore-all Break_: There's no difference between break and continue - break is just optimization
1501: break;
1502: }
1503: }
1504:
1505: return $interfaceClassNames;
1506: }
1507:
1508: /**
1509: * @param list<class-string> $interfaceClassNames
1510: *
1511: * @return list<class-string>
1512: */
1513: private function addEnumInterfaceClassNames(array $interfaceClassNames): array
1514: {
1515: assert($this->isEnum === true);
1516:
1517: $interfaceClassNames[] = UnitEnum::class;
1518:
1519: if ($this->isBackedEnum) {
1520: $interfaceClassNames[] = BackedEnum::class;
1521: }
1522:
1523: return $interfaceClassNames;
1524: }
1525:
1526: /** @return list<trait-string> */
1527: public function getTraitClassNames(): array
1528: {
1529: return $this->traitClassNames;
1530: }
1531:
1532: /**
1533: * Get the names of the traits used as an array of strings, if any are
1534: * defined. If this class does not have any defined traits, this will
1535: * return an empty array.
1536: *
1537: * @return list<trait-string>
1538: */
1539: public function getTraitNames(): array
1540: {
1541: return array_map(
1542: static function (ReflectionClass $trait): string {
1543: /** @psalm-var trait-string $traitName */
1544: $traitName = $trait->getName();
1545:
1546: return $traitName;
1547: },
1548: $this->getTraits(),
1549: );
1550: }
1551:
1552: /**
1553: * Return a list of the aliases used when importing traits for this class.
1554: * The returned array is in key/value pair in this format:.
1555: *
1556: * 'aliasedMethodName' => 'ActualClass::actualMethod'
1557: *
1558: * @return array<non-empty-string, non-empty-string>
1559: *
1560: * @example
1561: * // When reflecting a class such as:
1562: * class Foo
1563: * {
1564: * use MyTrait {
1565: * myTraitMethod as myAliasedMethod;
1566: * }
1567: * }
1568: * // This method would return
1569: * // ['myAliasedMethod' => 'MyTrait::myTraitMethod']
1570: */
1571: public function getTraitAliases(): array
1572: {
1573: if ($this->traitsData['aliases'] === []) {
1574: return [];
1575: }
1576:
1577: $traits = [];
1578: foreach ($this->getTraits() as $trait) {
1579: $traits[$trait->getName()] = $trait;
1580: }
1581: $traitAliases = [];
1582:
1583: foreach ($this->traitsData['aliases'] as $traitClassName => $traitAliasDefinitions) {
1584: foreach ($traitAliasDefinitions as $traitAliasDefinition) {
1585: if (!array_key_exists($traitClassName, $traits)) {
1586: continue;
1587: }
1588: if (! $traits[$traitClassName]->hasMethod($traitAliasDefinition['method'])) {
1589: continue;
1590: }
1591:
1592: $traitAliases[$traitAliasDefinition['alias']] = $this->traitsData['hashes'][$traitAliasDefinition['hash']];
1593: }
1594: }
1595:
1596: return $traitAliases;
1597: }
1598:
1599: /**
1600: * Returns data when importing traits for this class:
1601: *
1602: * 'aliases': List of the aliases used when importing traits. In format:
1603: *
1604: * 'traitClassName' => ['alias' => 'aliasedMethodName', 'method' => 'actualMethodName', 'hash' => 'traitClassName::actualMethodName'],
1605: *
1606: * Example:
1607: * // When reflecting a code such as:
1608: *
1609: * use MyTrait {
1610: * myTraitMethod as myAliasedMethod;
1611: * }
1612: *
1613: * // This method would return
1614: * // ['MyTrait' => ['alias' => 'myAliasedMethod', 'method' => 'myTraitMethod', 'hash' => 'mytrait::mytraitmethod']]
1615: *
1616: * 'modifiers': Used modifiers when importing traits. In format:
1617: *
1618: * 'methodName' => 'modifier'
1619: *
1620: * Example:
1621: * // When reflecting a code such as:
1622: *
1623: * use MyTrait {
1624: * myTraitMethod as public;
1625: * }
1626: *
1627: * // This method would return
1628: * // ['myTraitMethod' => 1]
1629: *
1630: * 'precedences': Precedences used when importing traits. In format:
1631: *
1632: * 'Class::method' => 'Class::method'
1633: *
1634: * Example:
1635: * // When reflecting a code such as:
1636: *
1637: * use MyTrait, MyTrait2 {
1638: * MyTrait2::foo insteadof MyTrait1;
1639: * }
1640: *
1641: * // This method would return
1642: * // ['MyTrait1::foo' => 'MyTrait2::foo']
1643: *
1644: * @return array{
1645: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1646: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1647: * precedences: array<non-empty-string, non-empty-string>,
1648: * hashes: array<non-empty-string, non-empty-string>,
1649: * }
1650: * @param ClassNode|InterfaceNode|TraitNode|EnumNode $node
1651: */
1652: private function computeTraitsData($node): array
1653: {
1654: $traitsData = [
1655: 'aliases' => [],
1656: 'modifiers' => [],
1657: 'precedences' => [],
1658: 'hashes' => [],
1659: ];
1660:
1661: foreach ($node->getTraitUses() as $traitUsage) {
1662: foreach ($traitUsage->adaptations as $adaptation) {
1663: $usedTraits = $adaptation->trait !== null ? [$adaptation->trait] : $traitUsage->traits;
1664: $traitsData = $this->processTraitAdaptation($adaptation, $usedTraits, $traitsData);
1665: }
1666: }
1667:
1668: return $traitsData;
1669: }
1670:
1671: /**
1672: * @phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName
1673: *
1674: * @param array<array-key, Node\Name> $usedTraits
1675: * @param array{
1676: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1677: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1678: * precedences: array<non-empty-string, non-empty-string>,
1679: * hashes: array<non-empty-string, non-empty-string>,
1680: * } $traitsData
1681: *
1682: * @return array{
1683: * aliases: array<trait-string, list<array{alias: non-empty-string, method: non-empty-string, hash: non-empty-string}>>,
1684: * modifiers: array<non-empty-string, int-mask-of<ReflectionMethodAdapter::IS_*>>,
1685: * precedences: array<non-empty-string, non-empty-string>,
1686: * hashes: array<non-empty-string, non-empty-string>,
1687: * }
1688: */
1689: private function processTraitAdaptation(Node\Stmt\TraitUseAdaptation $adaptation, array $usedTraits, array $traitsData): array
1690: {
1691: foreach ($usedTraits as $usedTrait) {
1692: $methodHash = $this->methodHash($usedTrait->toString(), $adaptation->method->toString());
1693: $lowerCasedMethodHash = $this->lowerCasedMethodHash($usedTrait->toString(), $adaptation->method->toString());
1694:
1695: $traitsData['hashes'][$lowerCasedMethodHash] = $methodHash;
1696:
1697: if ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
1698: if ($adaptation->newModifier !== null) {
1699: /** @var int-mask-of<ReflectionMethodAdapter::IS_*> $modifier */
1700: $modifier = $adaptation->newModifier;
1701: $traitsData['modifiers'][$lowerCasedMethodHash] = $modifier;
1702: }
1703:
1704: if ($adaptation->newName !== null) {
1705: // We need to save all possible combinations of trait and method names
1706: // The real aliases will be filtered in getters
1707: /** @var trait-string $usedTraitClassName */
1708: $usedTraitClassName = $usedTrait->toString();
1709: $traitsData['aliases'][$usedTraitClassName][] = [
1710: 'alias' => $adaptation->newName->name,
1711: 'method' => $adaptation->method->toString(),
1712: 'hash' => $lowerCasedMethodHash,
1713: ];
1714: }
1715: } elseif ($adaptation instanceof Node\Stmt\TraitUseAdaptation\Precedence) {
1716: foreach ($adaptation->insteadof as $insteadof) {
1717: $adaptationNameHash = $this->lowerCasedMethodHash($insteadof->toString(), $adaptation->method->toString());
1718:
1719: $traitsData['precedences'][$adaptationNameHash] = $lowerCasedMethodHash;
1720: }
1721: }
1722: }
1723:
1724: return $traitsData;
1725: }
1726:
1727: /**
1728: * @return non-empty-string
1729: *
1730: * @psalm-pure
1731: */
1732: private function methodHash(string $className, string $methodName): string
1733: {
1734: return sprintf(
1735: '%s::%s',
1736: $className,
1737: $methodName,
1738: );
1739: }
1740:
1741: /** @return non-empty-string */
1742: private function lowerCasedMethodHash(string $className, string $methodName): string
1743: {
1744: return strtolower($this->methodHash($className, $methodName));
1745: }
1746:
1747: /** @return list<class-string> */
1748: public function getInterfaceClassNames(): array
1749: {
1750: $implementsClassNames = $this->implementsClassNames;
1751:
1752: if ($this->isEnum) {
1753: $implementsClassNames = $this->addEnumInterfaceClassNames($implementsClassNames);
1754: }
1755:
1756: return $this->addStringableInterfaceClassName($implementsClassNames);
1757: }
1758:
1759: /**
1760: * Gets the interfaces.
1761: *
1762: * @link https://php.net/manual/en/reflectionclass.getinterfaces.php
1763: *
1764: * @return array<class-string, self> An associative array of interfaces, with keys as interface names and the array
1765: * values as {@see ReflectionClass} objects.
1766: */
1767: public function getInterfaces(): array
1768: {
1769: if ($this->cachedInterfaces !== null) {
1770: return $this->cachedInterfaces;
1771: }
1772:
1773: $interfaces = array_merge(
1774: [$this->getCurrentClassImplementedInterfacesIndexedByName()],
1775: array_map(
1776: static fn (self $parentClass): array => $parentClass->getCurrentClassImplementedInterfacesIndexedByName(),
1777: $this->getParentClasses(),
1778: ),
1779: );
1780:
1781: return $this->cachedInterfaces = array_merge(...array_reverse($interfaces));
1782: }
1783:
1784: /**
1785: * Get only the interfaces that this class implements (i.e. do not search
1786: * up parent classes etc.)
1787: *
1788: * @return array<class-string, self>
1789: */
1790: public function getImmediateInterfaces(): array
1791: {
1792: if ($this->isTrait) {
1793: return [];
1794: }
1795:
1796: $interfaces = [];
1797: foreach ($this->getInterfaceClassNames() as $interfaceClassName) {
1798: try {
1799: $interfaces[$interfaceClassName] = $this->reflector->reflectClass($interfaceClassName);
1800: } catch (IdentifierNotFound $exception) {
1801: continue;
1802: }
1803: }
1804:
1805: return $interfaces;
1806: }
1807:
1808: /**
1809: * Gets the interface names.
1810: *
1811: * @link https://php.net/manual/en/reflectionclass.getinterfacenames.php
1812: *
1813: * @return list<class-string> A numerical array with interface names as the values.
1814: */
1815: public function getInterfaceNames(): array
1816: {
1817: if ($this->cachedInterfaceNames !== null) {
1818: return $this->cachedInterfaceNames;
1819: }
1820:
1821: return $this->cachedInterfaceNames = array_values(array_map(
1822: static fn (self $interface): string => $interface->getName(),
1823: $this->getInterfaces(),
1824: ));
1825: }
1826:
1827: /**
1828: * Checks whether the given object is an instance.
1829: *
1830: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1831: */
1832: public function isInstance(object $object): bool
1833: {
1834: $className = $this->getName();
1835:
1836: // note: since $object was loaded, we can safely assume that $className is available in the current
1837: // php script execution context
1838: return $object instanceof $className;
1839: }
1840:
1841: /**
1842: * Checks whether the given class string is a subclass of this class.
1843: *
1844: * @link https://php.net/manual/en/reflectionclass.isinstance.php
1845: */
1846: public function isSubclassOf(string $className): bool
1847: {
1848: return in_array(
1849: ltrim($className, '\\'),
1850: $this->getParentClassNames(),
1851: true,
1852: );
1853: }
1854:
1855: /**
1856: * Checks whether this class implements the given interface.
1857: *
1858: * @link https://php.net/manual/en/reflectionclass.implementsinterface.php
1859: */
1860: public function implementsInterface(string $interfaceName): bool
1861: {
1862: return in_array(ltrim($interfaceName, '\\'), $this->getInterfaceNames(), true);
1863: }
1864:
1865: /**
1866: * Checks whether this reflection is an instantiable class
1867: *
1868: * @link https://php.net/manual/en/reflectionclass.isinstantiable.php
1869: */
1870: public function isInstantiable(): bool
1871: {
1872: // @TODO doesn't consider internal non-instantiable classes yet.
1873:
1874: if ($this->isAbstract()) {
1875: return false;
1876: }
1877:
1878: if ($this->isInterface()) {
1879: return false;
1880: }
1881:
1882: if ($this->isTrait()) {
1883: return false;
1884: }
1885:
1886: $constructor = $this->getConstructor();
1887:
1888: if ($constructor === null) {
1889: return true;
1890: }
1891:
1892: return $constructor->isPublic();
1893: }
1894:
1895: /**
1896: * Checks whether this is a reflection of a class that supports the clone operator
1897: *
1898: * @link https://php.net/manual/en/reflectionclass.iscloneable.php
1899: */
1900: public function isCloneable(): bool
1901: {
1902: if (! $this->isInstantiable()) {
1903: return false;
1904: }
1905:
1906: $cloneMethod = $this->getMethod('__clone');
1907:
1908: if ($cloneMethod === null) {
1909: return true;
1910: }
1911:
1912: return $cloneMethod->isPublic();
1913: }
1914:
1915: /**
1916: * Checks if iterateable
1917: *
1918: * @link https://php.net/manual/en/reflectionclass.isiterateable.php
1919: */
1920: public function isIterateable(): bool
1921: {
1922: return $this->isInstantiable() && $this->implementsInterface(Traversable::class);
1923: }
1924:
1925: public function isEnum(): bool
1926: {
1927: return $this->isEnum;
1928: }
1929:
1930: /** @return array<class-string, ReflectionClass> */
1931: private function getCurrentClassImplementedInterfacesIndexedByName(): array
1932: {
1933: if ($this->isTrait) {
1934: return [];
1935: }
1936:
1937: if ($this->isInterface) {
1938: // assumption: first key is the current interface
1939: return array_slice($this->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()), 1);
1940: }
1941:
1942: $interfaces = [];
1943: foreach ($this->getInterfaceClassNames() as $name) {
1944: try {
1945: $interface = $this->reflector->reflectClass($name);
1946: foreach ($interface->getInterfacesHierarchy(AlreadyVisitedClasses::createEmpty()) as $n => $i) {
1947: $interfaces[$n] = $i;
1948: }
1949: } catch (IdentifierNotFound $exception) {
1950: continue;
1951: }
1952: }
1953:
1954: return $interfaces;
1955: }
1956:
1957: /**
1958: * This method allows us to retrieve all interfaces parent of this interface. Do not use on class nodes!
1959: *
1960: * @return array<class-string, ReflectionClass> parent interfaces of this interface
1961: */
1962: private function getInterfacesHierarchy(AlreadyVisitedClasses $alreadyVisitedClasses): array
1963: {
1964: if (! $this->isInterface) {
1965: return [];
1966: }
1967:
1968: $interfaceClassName = $this->getName();
1969: $alreadyVisitedClasses->push($interfaceClassName);
1970:
1971: /** @var array<class-string, self> $interfaces */
1972: $interfaces = [$interfaceClassName => $this];
1973: foreach ($this->getImmediateInterfaces() as $interface) {
1974: $alreadyVisitedClassesCopyForInterface = clone $alreadyVisitedClasses;
1975: foreach ($interface->getInterfacesHierarchy($alreadyVisitedClassesCopyForInterface) as $extendedInterfaceName => $extendedInterface) {
1976: $interfaces[$extendedInterfaceName] = $extendedInterface;
1977: }
1978: }
1979:
1980: return $interfaces;
1981: }
1982:
1983: /**
1984: * Get the value of a static property, if it exists. Throws a
1985: * PropertyDoesNotExist exception if it does not exist or is not static.
1986: * (note, differs very slightly from internal reflection behaviour)
1987: *
1988: * @param non-empty-string $propertyName
1989: *
1990: * @throws ClassDoesNotExist
1991: * @throws NoObjectProvided
1992: * @throws NotAnObject
1993: * @throws ObjectNotInstanceOfClass
1994: * @return mixed
1995: */
1996: public function getStaticPropertyValue(string $propertyName)
1997: {
1998: $property = $this->getProperty($propertyName);
1999:
2000: if (! $property || ! $property->isStatic()) {
2001: throw PropertyDoesNotExist::fromName($propertyName);
2002: }
2003:
2004: return $property->getValue();
2005: }
2006:
2007: /**
2008: * Set the value of a static property
2009: *
2010: * @param non-empty-string $propertyName
2011: *
2012: * @throws ClassDoesNotExist
2013: * @throws NoObjectProvided
2014: * @throws NotAnObject
2015: * @throws ObjectNotInstanceOfClass
2016: * @param mixed $value
2017: */
2018: public function setStaticPropertyValue(string $propertyName, $value): void
2019: {
2020: $property = $this->getProperty($propertyName);
2021:
2022: if (! $property || ! $property->isStatic()) {
2023: throw PropertyDoesNotExist::fromName($propertyName);
2024: }
2025:
2026: $property->setValue($value);
2027: }
2028:
2029: /** @return array<non-empty-string, mixed> */
2030: public function getStaticProperties(): array
2031: {
2032: $staticProperties = [];
2033:
2034: foreach ($this->getProperties() as $property) {
2035: if (! $property->isStatic()) {
2036: continue;
2037: }
2038:
2039: /** @psalm-suppress MixedAssignment */
2040: $staticProperties[$property->getName()] = $property->getValue();
2041: }
2042:
2043: return $staticProperties;
2044: }
2045:
2046: /** @return list<ReflectionAttribute> */
2047: public function getAttributes(): array
2048: {
2049: return $this->attributes;
2050: }
2051:
2052: /** @return list<ReflectionAttribute> */
2053: public function getAttributesByName(string $name): array
2054: {
2055: if ($this->attributes === []) {
2056: return [];
2057: }
2058:
2059: return ReflectionAttributeHelper::filterAttributesByName($this->attributes, $name);
2060: }
2061:
2062: /**
2063: * @param class-string $className
2064: *
2065: * @return list<ReflectionAttribute>
2066: */
2067: public function getAttributesByInstance(string $className): array
2068: {
2069: return ReflectionAttributeHelper::filterAttributesByInstance($this->getAttributes(), $className);
2070: }
2071: }
2072: