1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection;
4:
5: use Attribute;
6: use PhpParser\Node\Arg;
7: use PhpParser\Node\Expr\StaticCall;
8: use PhpParser\Node\Identifier;
9: use PhpParser\Node\Name\FullyQualified;
10: use PHPStan\Analyser\ArgumentsNormalizer;
11: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
12: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum;
13: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnumBackedCase;
14: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod;
15: use PHPStan\Php\PhpVersion;
16: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
17: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
18: use PHPStan\PhpDoc\StubPhpDocProvider;
19: use PHPStan\PhpDoc\Tag\ExtendsTag;
20: use PHPStan\PhpDoc\Tag\ImplementsTag;
21: use PHPStan\PhpDoc\Tag\MethodTag;
22: use PHPStan\PhpDoc\Tag\MixinTag;
23: use PHPStan\PhpDoc\Tag\PropertyTag;
24: use PHPStan\PhpDoc\Tag\TemplateTag;
25: use PHPStan\PhpDoc\Tag\TypeAliasImportTag;
26: use PHPStan\PhpDoc\Tag\TypeAliasTag;
27: use PHPStan\Reflection\Php\PhpClassReflectionExtension;
28: use PHPStan\Reflection\Php\PhpPropertyReflection;
29: use PHPStan\ShouldNotHappenException;
30: use PHPStan\Type\CircularTypeAliasDefinitionException;
31: use PHPStan\Type\Constant\ConstantIntegerType;
32: use PHPStan\Type\ErrorType;
33: use PHPStan\Type\FileTypeMapper;
34: use PHPStan\Type\Generic\GenericObjectType;
35: use PHPStan\Type\Generic\TemplateTypeFactory;
36: use PHPStan\Type\Generic\TemplateTypeHelper;
37: use PHPStan\Type\Generic\TemplateTypeMap;
38: use PHPStan\Type\Generic\TemplateTypeScope;
39: use PHPStan\Type\Type;
40: use PHPStan\Type\TypeAlias;
41: use PHPStan\Type\TypehintHelper;
42: use PHPStan\Type\VerbosityLevel;
43: use ReflectionException;
44: use stdClass;
45: use function array_diff;
46: use function array_filter;
47: use function array_key_exists;
48: use function array_map;
49: use function array_merge;
50: use function array_shift;
51: use function array_unique;
52: use function array_values;
53: use function count;
54: use function implode;
55: use function in_array;
56: use function is_bool;
57: use function is_file;
58: use function is_int;
59: use function reset;
60: use function sprintf;
61: use function strtolower;
62:
63: /** @api */
64: class ClassReflection
65: {
66:
67: /** @var ExtendedMethodReflection[] */
68: private array $methods = [];
69:
70: /** @var PropertyReflection[] */
71: private array $properties = [];
72:
73: /** @var ConstantReflection[] */
74: private array $constants = [];
75:
76: /** @var int[]|null */
77: private ?array $classHierarchyDistances = null;
78:
79: private ?string $deprecatedDescription = null;
80:
81: private ?bool $isDeprecated = null;
82:
83: private ?bool $isGeneric = null;
84:
85: private ?bool $isInternal = null;
86:
87: private ?bool $isFinal = null;
88:
89: private ?bool $isImmutable = null;
90:
91: private ?bool $hasConsistentConstructor = null;
92:
93: private ?TemplateTypeMap $templateTypeMap = null;
94:
95: private ?TemplateTypeMap $activeTemplateTypeMap = null;
96:
97: /** @var array<string,ClassReflection>|null */
98: private ?array $ancestors = null;
99:
100: private ?string $cacheKey = null;
101:
102: /** @var array<string, bool> */
103: private array $subclasses = [];
104:
105: private string|false|null $filename = false;
106:
107: private string|false|null $reflectionDocComment = false;
108:
109: private false|ResolvedPhpDocBlock $resolvedPhpDocBlock = false;
110:
111: /** @var ClassReflection[]|null */
112: private ?array $cachedInterfaces = null;
113:
114: private ClassReflection|false|null $cachedParentClass = false;
115:
116: /** @var array<string, TypeAlias>|null */
117: private ?array $typeAliases = null;
118:
119: /** @var array<string, true> */
120: private static array $resolvingTypeAliasImports = [];
121:
122: /**
123: * @param PropertiesClassReflectionExtension[] $propertiesClassReflectionExtensions
124: * @param MethodsClassReflectionExtension[] $methodsClassReflectionExtensions
125: * @param AllowedSubTypesClassReflectionExtension[] $allowedSubTypesClassReflectionExtensions
126: */
127: public function __construct(
128: private ReflectionProvider $reflectionProvider,
129: private InitializerExprTypeResolver $initializerExprTypeResolver,
130: private FileTypeMapper $fileTypeMapper,
131: private StubPhpDocProvider $stubPhpDocProvider,
132: private PhpDocInheritanceResolver $phpDocInheritanceResolver,
133: private PhpVersion $phpVersion,
134: private array $propertiesClassReflectionExtensions,
135: private array $methodsClassReflectionExtensions,
136: private array $allowedSubTypesClassReflectionExtensions,
137: private string $displayName,
138: private ReflectionClass|ReflectionEnum $reflection,
139: private ?string $anonymousFilename,
140: private ?TemplateTypeMap $resolvedTemplateTypeMap,
141: private ?ResolvedPhpDocBlock $stubPhpDocBlock,
142: private ?string $extraCacheKey = null,
143: )
144: {
145: }
146:
147: public function getNativeReflection(): ReflectionClass|ReflectionEnum
148: {
149: return $this->reflection;
150: }
151:
152: public function getFileName(): ?string
153: {
154: if (!is_bool($this->filename)) {
155: return $this->filename;
156: }
157:
158: if ($this->anonymousFilename !== null) {
159: return $this->filename = $this->anonymousFilename;
160: }
161: $fileName = $this->reflection->getFileName();
162: if ($fileName === false) {
163: return $this->filename = null;
164: }
165:
166: if (!is_file($fileName)) {
167: return $this->filename = null;
168: }
169:
170: return $this->filename = $fileName;
171: }
172:
173: /**
174: * @deprecated Use getFileName()
175: */
176: public function getFileNameWithPhpDocs(): ?string
177: {
178: return $this->getFileName();
179: }
180:
181: public function getParentClass(): ?ClassReflection
182: {
183: if (!is_bool($this->cachedParentClass)) {
184: return $this->cachedParentClass;
185: }
186:
187: $parentClass = $this->reflection->getParentClass();
188:
189: if ($parentClass === false) {
190: return $this->cachedParentClass = null;
191: }
192:
193: $extendsTag = $this->getFirstExtendsTag();
194:
195: if ($extendsTag !== null && $this->isValidAncestorType($extendsTag->getType(), [$parentClass->getName()])) {
196: $extendedType = $extendsTag->getType();
197:
198: if ($this->isGeneric()) {
199: $extendedType = TemplateTypeHelper::resolveTemplateTypes(
200: $extendedType,
201: $this->getPossiblyIncompleteActiveTemplateTypeMap(),
202: true,
203: );
204: }
205:
206: if (!$extendedType instanceof GenericObjectType) {
207: return $this->reflectionProvider->getClass($parentClass->getName());
208: }
209:
210: return $extendedType->getClassReflection() ?? $this->reflectionProvider->getClass($parentClass->getName());
211: }
212:
213: $parentReflection = $this->reflectionProvider->getClass($parentClass->getName());
214: if ($parentReflection->isGeneric()) {
215: return $parentReflection->withTypes(
216: array_values($parentReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()),
217: );
218: }
219:
220: $this->cachedParentClass = $parentReflection;
221:
222: return $parentReflection;
223: }
224:
225: /**
226: * @return class-string
227: */
228: public function getName(): string
229: {
230: return $this->reflection->getName();
231: }
232:
233: public function getDisplayName(bool $withTemplateTypes = true): string
234: {
235: $name = $this->displayName;
236:
237: if (
238: $withTemplateTypes === false
239: || $this->resolvedTemplateTypeMap === null
240: || count($this->resolvedTemplateTypeMap->getTypes()) === 0
241: ) {
242: return $name;
243: }
244:
245: return $name . '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::typeOnly()), $this->getActiveTemplateTypeMap()->getTypes())) . '>';
246: }
247:
248: public function getCacheKey(): string
249: {
250: $cacheKey = $this->cacheKey;
251: if ($cacheKey !== null) {
252: return $this->cacheKey;
253: }
254:
255: $cacheKey = $this->displayName;
256:
257: if ($this->resolvedTemplateTypeMap !== null) {
258: $cacheKey .= '<' . implode(',', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::cache()), $this->resolvedTemplateTypeMap->getTypes())) . '>';
259: }
260:
261: if ($this->extraCacheKey !== null) {
262: $cacheKey .= '-' . $this->extraCacheKey;
263: }
264:
265: $this->cacheKey = $cacheKey;
266:
267: return $cacheKey;
268: }
269:
270: /**
271: * @return int[]
272: */
273: public function getClassHierarchyDistances(): array
274: {
275: if ($this->classHierarchyDistances === null) {
276: $distance = 0;
277: $distances = [
278: $this->getName() => $distance,
279: ];
280: $currentClassReflection = $this->getNativeReflection();
281: foreach ($this->collectTraits($this->getNativeReflection()) as $trait) {
282: $distance++;
283: if (array_key_exists($trait->getName(), $distances)) {
284: continue;
285: }
286:
287: $distances[$trait->getName()] = $distance;
288: }
289:
290: while ($currentClassReflection->getParentClass() !== false) {
291: $distance++;
292: $parentClassName = $currentClassReflection->getParentClass()->getName();
293: if (!array_key_exists($parentClassName, $distances)) {
294: $distances[$parentClassName] = $distance;
295: }
296: $currentClassReflection = $currentClassReflection->getParentClass();
297: foreach ($this->collectTraits($currentClassReflection) as $trait) {
298: $distance++;
299: if (array_key_exists($trait->getName(), $distances)) {
300: continue;
301: }
302:
303: $distances[$trait->getName()] = $distance;
304: }
305: }
306: foreach ($this->getNativeReflection()->getInterfaces() as $interface) {
307: $distance++;
308: if (array_key_exists($interface->getName(), $distances)) {
309: continue;
310: }
311:
312: $distances[$interface->getName()] = $distance;
313: }
314:
315: $this->classHierarchyDistances = $distances;
316: }
317:
318: return $this->classHierarchyDistances;
319: }
320:
321: /**
322: * @return ReflectionClass[]
323: */
324: private function collectTraits(ReflectionClass|ReflectionEnum $class): array
325: {
326: $traits = [];
327: $traitsLeftToAnalyze = $class->getTraits();
328:
329: while (count($traitsLeftToAnalyze) !== 0) {
330: $trait = reset($traitsLeftToAnalyze);
331: $traits[] = $trait;
332:
333: foreach ($trait->getTraits() as $subTrait) {
334: if (in_array($subTrait, $traits, true)) {
335: continue;
336: }
337:
338: $traitsLeftToAnalyze[] = $subTrait;
339: }
340:
341: array_shift($traitsLeftToAnalyze);
342: }
343:
344: return $traits;
345: }
346:
347: public function allowsDynamicProperties(): bool
348: {
349: if (!$this->phpVersion->deprecatesDynamicProperties()) {
350: return true;
351: }
352:
353: if ($this->isReadOnly()) {
354: return false;
355: }
356:
357: if ($this->is(stdClass::class)) {
358: return true;
359: }
360:
361: $class = $this;
362: $attributes = $class->reflection->getAttributes('AllowDynamicProperties');
363: while (count($attributes) === 0 && $class->getParentClass() !== null) {
364: $attributes = $class->getParentClass()->reflection->getAttributes('AllowDynamicProperties');
365: $class = $class->getParentClass();
366: }
367:
368: return count($attributes) > 0;
369: }
370:
371: private function allowsDynamicPropertiesExtensions(): bool
372: {
373: if ($this->allowsDynamicProperties()) {
374: return true;
375: }
376:
377: return $this->hasNativeMethod('__get') || $this->hasNativeMethod('__set') || $this->hasNativeMethod('__isset');
378: }
379:
380: public function hasProperty(string $propertyName): bool
381: {
382: if ($this->isEnum()) {
383: return $this->hasNativeProperty($propertyName);
384: }
385:
386: foreach ($this->propertiesClassReflectionExtensions as $i => $extension) {
387: if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) {
388: continue;
389: }
390: if ($extension->hasProperty($this, $propertyName)) {
391: return true;
392: }
393: }
394:
395: return false;
396: }
397:
398: public function hasMethod(string $methodName): bool
399: {
400: foreach ($this->methodsClassReflectionExtensions as $extension) {
401: if ($extension->hasMethod($this, $methodName)) {
402: return true;
403: }
404: }
405:
406: return false;
407: }
408:
409: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
410: {
411: $key = $methodName;
412: if ($scope->isInClass()) {
413: $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey());
414: }
415: if (!isset($this->methods[$key])) {
416: foreach ($this->methodsClassReflectionExtensions as $extension) {
417: if (!$extension->hasMethod($this, $methodName)) {
418: continue;
419: }
420:
421: $method = $this->wrapExtendedMethod($extension->getMethod($this, $methodName));
422: if ($scope->canCallMethod($method)) {
423: return $this->methods[$key] = $method;
424: }
425: $this->methods[$key] = $method;
426: }
427: }
428:
429: if (!isset($this->methods[$key])) {
430: throw new MissingMethodFromReflectionException($this->getName(), $methodName);
431: }
432:
433: return $this->methods[$key];
434: }
435:
436: private function wrapExtendedMethod(MethodReflection $method): ExtendedMethodReflection
437: {
438: if ($method instanceof ExtendedMethodReflection) {
439: return $method;
440: }
441:
442: return new WrappedExtendedMethodReflection($method);
443: }
444:
445: public function hasNativeMethod(string $methodName): bool
446: {
447: return $this->getPhpExtension()->hasNativeMethod($this, $methodName);
448: }
449:
450: public function getNativeMethod(string $methodName): ExtendedMethodReflection
451: {
452: if (!$this->hasNativeMethod($methodName)) {
453: throw new MissingMethodFromReflectionException($this->getName(), $methodName);
454: }
455: return $this->getPhpExtension()->getNativeMethod($this, $methodName);
456: }
457:
458: public function hasConstructor(): bool
459: {
460: return $this->findConstructor() !== null;
461: }
462:
463: public function getConstructor(): ExtendedMethodReflection
464: {
465: $constructor = $this->findConstructor();
466: if ($constructor === null) {
467: throw new ShouldNotHappenException();
468: }
469: return $this->getNativeMethod($constructor->getName());
470: }
471:
472: private function findConstructor(): ?ReflectionMethod
473: {
474: $constructor = $this->reflection->getConstructor();
475: if ($constructor === null) {
476: return null;
477: }
478:
479: if ($this->phpVersion->supportsLegacyConstructor()) {
480: return $constructor;
481: }
482:
483: if (strtolower($constructor->getName()) !== '__construct') {
484: return null;
485: }
486:
487: return $constructor;
488: }
489:
490: private function getPhpExtension(): PhpClassReflectionExtension
491: {
492: $extension = $this->methodsClassReflectionExtensions[0];
493: if (!$extension instanceof PhpClassReflectionExtension) {
494: throw new ShouldNotHappenException();
495: }
496:
497: return $extension;
498: }
499:
500: /** @internal */
501: public function evictPrivateSymbols(): void
502: {
503: foreach ($this->constants as $name => $constant) {
504: if (!$constant->isPrivate()) {
505: continue;
506: }
507:
508: unset($this->constants[$name]);
509: }
510: foreach ($this->properties as $name => $property) {
511: if (!$property->isPrivate()) {
512: continue;
513: }
514:
515: unset($this->properties[$name]);
516: }
517: foreach ($this->methods as $name => $method) {
518: if (!$method->isPrivate()) {
519: continue;
520: }
521:
522: unset($this->methods[$name]);
523: }
524: $this->getPhpExtension()->evictPrivateSymbols($this->getCacheKey());
525: }
526:
527: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
528: {
529: if ($this->isEnum()) {
530: return $this->getNativeProperty($propertyName);
531: }
532:
533: $key = $propertyName;
534: if ($scope->isInClass()) {
535: $key = sprintf('%s-%s', $key, $scope->getClassReflection()->getCacheKey());
536: }
537: if (!isset($this->properties[$key])) {
538: foreach ($this->propertiesClassReflectionExtensions as $i => $extension) {
539: if ($i > 0 && !$this->allowsDynamicPropertiesExtensions()) {
540: continue;
541: }
542: if (!$extension->hasProperty($this, $propertyName)) {
543: continue;
544: }
545:
546: $property = $extension->getProperty($this, $propertyName);
547: if ($scope->canAccessProperty($property)) {
548: return $this->properties[$key] = $property;
549: }
550: $this->properties[$key] = $property;
551: }
552: }
553:
554: if (!isset($this->properties[$key])) {
555: throw new MissingPropertyFromReflectionException($this->getName(), $propertyName);
556: }
557:
558: return $this->properties[$key];
559: }
560:
561: public function hasNativeProperty(string $propertyName): bool
562: {
563: return $this->getPhpExtension()->hasProperty($this, $propertyName);
564: }
565:
566: public function getNativeProperty(string $propertyName): PhpPropertyReflection
567: {
568: if (!$this->hasNativeProperty($propertyName)) {
569: throw new MissingPropertyFromReflectionException($this->getName(), $propertyName);
570: }
571:
572: return $this->getPhpExtension()->getNativeProperty($this, $propertyName);
573: }
574:
575: public function isAbstract(): bool
576: {
577: return $this->reflection->isAbstract();
578: }
579:
580: public function isInterface(): bool
581: {
582: return $this->reflection->isInterface();
583: }
584:
585: public function isTrait(): bool
586: {
587: return $this->reflection->isTrait();
588: }
589:
590: public function isEnum(): bool
591: {
592: return $this->reflection->isEnum();
593: }
594:
595: public function isReadOnly(): bool
596: {
597: return $this->reflection->isReadOnly();
598: }
599:
600: public function isBackedEnum(): bool
601: {
602: if (!$this->reflection instanceof ReflectionEnum) {
603: return false;
604: }
605:
606: return $this->reflection->isBacked();
607: }
608:
609: public function getBackedEnumType(): ?Type
610: {
611: if (!$this->reflection instanceof ReflectionEnum) {
612: return null;
613: }
614:
615: if (!$this->reflection->isBacked()) {
616: return null;
617: }
618:
619: $reflectionType = $this->reflection->getBackingType();
620: if ($reflectionType === null) {
621: return null;
622: }
623:
624: return TypehintHelper::decideTypeFromReflection($reflectionType);
625: }
626:
627: public function hasEnumCase(string $name): bool
628: {
629: if (!$this->isEnum()) {
630: return false;
631: }
632:
633: if (!$this->reflection instanceof ReflectionEnum) {
634: return false;
635: }
636:
637: return $this->reflection->hasCase($name);
638: }
639:
640: /**
641: * @return array<string, EnumCaseReflection>
642: */
643: public function getEnumCases(): array
644: {
645: if (!$this->reflection instanceof ReflectionEnum) {
646: throw new ShouldNotHappenException();
647: }
648:
649: $cases = [];
650: foreach ($this->reflection->getCases() as $case) {
651: $valueType = null;
652: if ($case instanceof ReflectionEnumBackedCase) {
653: $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
654: }
655: /** @var string $caseName */
656: $caseName = $case->getName();
657: $cases[$caseName] = new EnumCaseReflection($this, $caseName, $valueType);
658: }
659:
660: return $cases;
661: }
662:
663: public function getEnumCase(string $name): EnumCaseReflection
664: {
665: if (!$this->hasEnumCase($name)) {
666: throw new ShouldNotHappenException(sprintf('Enum case %s::%s does not exist.', $this->getDisplayName(), $name));
667: }
668:
669: if (!$this->reflection instanceof ReflectionEnum) {
670: throw new ShouldNotHappenException();
671: }
672:
673: $case = $this->reflection->getCase($name);
674: $valueType = null;
675: if ($case instanceof ReflectionEnumBackedCase) {
676: $valueType = $this->initializerExprTypeResolver->getType($case->getValueExpression(), InitializerExprContext::fromClassReflection($this));
677: }
678:
679: return new EnumCaseReflection($this, $name, $valueType);
680: }
681:
682: public function isClass(): bool
683: {
684: return !$this->isInterface() && !$this->isTrait() && !$this->isEnum();
685: }
686:
687: public function isAnonymous(): bool
688: {
689: return $this->anonymousFilename !== null;
690: }
691:
692: public function is(string $className): bool
693: {
694: return $this->getName() === $className || $this->isSubclassOf($className);
695: }
696:
697: public function isSubclassOf(string $className): bool
698: {
699: if (isset($this->subclasses[$className])) {
700: return $this->subclasses[$className];
701: }
702:
703: if (!$this->reflectionProvider->hasClass($className)) {
704: return $this->subclasses[$className] = false;
705: }
706:
707: try {
708: return $this->subclasses[$className] = $this->reflection->isSubclassOf($className);
709: } catch (ReflectionException) {
710: return $this->subclasses[$className] = false;
711: }
712: }
713:
714: public function implementsInterface(string $className): bool
715: {
716: try {
717: return $this->reflection->implementsInterface($className);
718: } catch (ReflectionException) {
719: return false;
720: }
721: }
722:
723: /**
724: * @return ClassReflection[]
725: */
726: public function getParents(): array
727: {
728: $parents = [];
729: $parent = $this->getParentClass();
730: while ($parent !== null) {
731: $parents[] = $parent;
732: $parent = $parent->getParentClass();
733: }
734:
735: return $parents;
736: }
737:
738: /**
739: * @return ClassReflection[]
740: */
741: public function getInterfaces(): array
742: {
743: if ($this->cachedInterfaces !== null) {
744: return $this->cachedInterfaces;
745: }
746:
747: $interfaces = $this->getImmediateInterfaces();
748: $immediateInterfaces = $interfaces;
749: $parent = $this->getParentClass();
750: while ($parent !== null) {
751: foreach ($parent->getImmediateInterfaces() as $parentInterface) {
752: $interfaces[$parentInterface->getName()] = $parentInterface;
753: foreach ($this->collectInterfaces($parentInterface) as $parentInterfaceInterface) {
754: $interfaces[$parentInterfaceInterface->getName()] = $parentInterfaceInterface;
755: }
756: }
757:
758: $parent = $parent->getParentClass();
759: }
760:
761: foreach ($immediateInterfaces as $immediateInterface) {
762: foreach ($this->collectInterfaces($immediateInterface) as $interfaceInterface) {
763: $interfaces[$interfaceInterface->getName()] = $interfaceInterface;
764: }
765: }
766:
767: $this->cachedInterfaces = $interfaces;
768:
769: return $interfaces;
770: }
771:
772: /**
773: * @return ClassReflection[]
774: */
775: private function collectInterfaces(ClassReflection $interface): array
776: {
777: $interfaces = [];
778: foreach ($interface->getImmediateInterfaces() as $immediateInterface) {
779: $interfaces[$immediateInterface->getName()] = $immediateInterface;
780: foreach ($this->collectInterfaces($immediateInterface) as $immediateInterfaceInterface) {
781: $interfaces[$immediateInterfaceInterface->getName()] = $immediateInterfaceInterface;
782: }
783: }
784:
785: return $interfaces;
786: }
787:
788: /**
789: * @return ClassReflection[]
790: */
791: public function getImmediateInterfaces(): array
792: {
793: $indirectInterfaceNames = [];
794: $parent = $this->getParentClass();
795: while ($parent !== null) {
796: foreach ($parent->getNativeReflection()->getInterfaceNames() as $parentInterfaceName) {
797: $indirectInterfaceNames[] = $parentInterfaceName;
798: }
799:
800: $parent = $parent->getParentClass();
801: }
802:
803: foreach ($this->getNativeReflection()->getInterfaces() as $interfaceInterface) {
804: foreach ($interfaceInterface->getInterfaceNames() as $interfaceInterfaceName) {
805: $indirectInterfaceNames[] = $interfaceInterfaceName;
806: }
807: }
808:
809: if ($this->reflection->isInterface()) {
810: $implementsTags = $this->getExtendsTags();
811: } else {
812: $implementsTags = $this->getImplementsTags();
813: }
814:
815: $immediateInterfaceNames = array_diff($this->getNativeReflection()->getInterfaceNames(), $indirectInterfaceNames);
816: $immediateInterfaces = [];
817: foreach ($immediateInterfaceNames as $immediateInterfaceName) {
818: if (!$this->reflectionProvider->hasClass($immediateInterfaceName)) {
819: continue;
820: }
821:
822: $immediateInterface = $this->reflectionProvider->getClass($immediateInterfaceName);
823: if (array_key_exists($immediateInterface->getName(), $implementsTags)) {
824: $implementsTag = $implementsTags[$immediateInterface->getName()];
825: $implementedType = $implementsTag->getType();
826: if ($this->isGeneric()) {
827: $implementedType = TemplateTypeHelper::resolveTemplateTypes(
828: $implementedType,
829: $this->getPossiblyIncompleteActiveTemplateTypeMap(),
830: true,
831: );
832: }
833:
834: if (
835: $implementedType instanceof GenericObjectType
836: && $implementedType->getClassReflection() !== null
837: ) {
838: $immediateInterfaces[$immediateInterface->getName()] = $implementedType->getClassReflection();
839: continue;
840: }
841: }
842:
843: if ($immediateInterface->isGeneric()) {
844: $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface->withTypes(
845: array_values($immediateInterface->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()),
846: );
847: continue;
848: }
849:
850: $immediateInterfaces[$immediateInterface->getName()] = $immediateInterface;
851: }
852:
853: return $immediateInterfaces;
854: }
855:
856: /**
857: * @return array<string, ClassReflection>
858: */
859: public function getTraits(bool $recursive = false): array
860: {
861: $traits = [];
862:
863: if ($recursive) {
864: foreach ($this->collectTraits($this->getNativeReflection()) as $trait) {
865: $traits[$trait->getName()] = $trait;
866: }
867: } else {
868: $traits = $this->getNativeReflection()->getTraits();
869: }
870:
871: $traits = array_map(fn (ReflectionClass $trait): ClassReflection => $this->reflectionProvider->getClass($trait->getName()), $traits);
872:
873: if ($recursive) {
874: $parentClass = $this->getNativeReflection()->getParentClass();
875:
876: if ($parentClass !== false) {
877: return array_merge(
878: $traits,
879: $this->reflectionProvider->getClass($parentClass->getName())->getTraits(true),
880: );
881: }
882: }
883:
884: return $traits;
885: }
886:
887: /**
888: * @return string[]
889: */
890: public function getParentClassesNames(): array
891: {
892: $parentNames = [];
893: $currentClassReflection = $this;
894: while ($currentClassReflection->getParentClass() !== null) {
895: $parentNames[] = $currentClassReflection->getParentClass()->getName();
896: $currentClassReflection = $currentClassReflection->getParentClass();
897: }
898:
899: return $parentNames;
900: }
901:
902: public function hasConstant(string $name): bool
903: {
904: if (!$this->getNativeReflection()->hasConstant($name)) {
905: return false;
906: }
907:
908: $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name);
909: if ($reflectionConstant === false) {
910: return false;
911: }
912:
913: return $this->reflectionProvider->hasClass($reflectionConstant->getDeclaringClass()->getName());
914: }
915:
916: public function getConstant(string $name): ConstantReflection
917: {
918: if (!isset($this->constants[$name])) {
919: $reflectionConstant = $this->getNativeReflection()->getReflectionConstant($name);
920: if ($reflectionConstant === false) {
921: throw new MissingConstantFromReflectionException($this->getName(), $name);
922: }
923:
924: $declaringClass = $this->reflectionProvider->getClass($reflectionConstant->getDeclaringClass()->getName());
925: $fileName = $declaringClass->getFileName();
926: $phpDocType = null;
927: $resolvedPhpDoc = $this->stubPhpDocProvider->findClassConstantPhpDoc(
928: $declaringClass->getName(),
929: $name,
930: );
931: if ($resolvedPhpDoc === null) {
932: $docComment = null;
933: if ($reflectionConstant->getDocComment() !== false) {
934: $docComment = $reflectionConstant->getDocComment();
935: }
936: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForConstant(
937: $docComment,
938: $declaringClass,
939: $fileName,
940: $name,
941: );
942: }
943:
944: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
945: $isDeprecated = $resolvedPhpDoc->isDeprecated();
946: $isInternal = $resolvedPhpDoc->isInternal();
947: $varTags = $resolvedPhpDoc->getVarTags();
948: if (isset($varTags[0]) && count($varTags) === 1) {
949: $phpDocType = $varTags[0]->getType();
950: }
951:
952: $this->constants[$name] = new ClassConstantReflection(
953: $this->initializerExprTypeResolver,
954: $declaringClass,
955: $reflectionConstant,
956: $phpDocType,
957: $deprecatedDescription,
958: $isDeprecated,
959: $isInternal,
960: );
961: }
962: return $this->constants[$name];
963: }
964:
965: public function hasTraitUse(string $traitName): bool
966: {
967: return in_array($traitName, $this->getTraitNames(), true);
968: }
969:
970: /**
971: * @return string[]
972: */
973: private function getTraitNames(): array
974: {
975: $class = $this->reflection;
976: $traitNames = $class->getTraitNames();
977: while ($class->getParentClass() !== false) {
978: $traitNames = array_values(array_unique(array_merge($traitNames, $class->getParentClass()->getTraitNames())));
979: $class = $class->getParentClass();
980: }
981:
982: return $traitNames;
983: }
984:
985: /**
986: * @return array<string, TypeAlias>
987: */
988: public function getTypeAliases(): array
989: {
990: if ($this->typeAliases === null) {
991: $resolvedPhpDoc = $this->getResolvedPhpDoc();
992: if ($resolvedPhpDoc === null) {
993: return $this->typeAliases = [];
994: }
995:
996: $typeAliasImportTags = $resolvedPhpDoc->getTypeAliasImportTags();
997: $typeAliasTags = $resolvedPhpDoc->getTypeAliasTags();
998:
999: // prevent circular imports
1000: if (array_key_exists($this->getName(), self::$resolvingTypeAliasImports)) {
1001: throw new CircularTypeAliasDefinitionException();
1002: }
1003:
1004: self::$resolvingTypeAliasImports[$this->getName()] = true;
1005:
1006: $importedAliases = array_map(function (TypeAliasImportTag $typeAliasImportTag): ?TypeAlias {
1007: $importedAlias = $typeAliasImportTag->getImportedAlias();
1008: $importedFromClassName = $typeAliasImportTag->getImportedFrom();
1009:
1010: if (!$this->reflectionProvider->hasClass($importedFromClassName)) {
1011: return null;
1012: }
1013:
1014: $importedFromReflection = $this->reflectionProvider->getClass($importedFromClassName);
1015:
1016: try {
1017: $typeAliases = $importedFromReflection->getTypeAliases();
1018: } catch (CircularTypeAliasDefinitionException) {
1019: return TypeAlias::invalid();
1020: }
1021:
1022: if (!array_key_exists($importedAlias, $typeAliases)) {
1023: return null;
1024: }
1025:
1026: return $typeAliases[$importedAlias];
1027: }, $typeAliasImportTags);
1028:
1029: unset(self::$resolvingTypeAliasImports[$this->getName()]);
1030:
1031: $localAliases = array_map(static fn (TypeAliasTag $typeAliasTag): TypeAlias => $typeAliasTag->getTypeAlias(), $typeAliasTags);
1032:
1033: $this->typeAliases = array_filter(
1034: array_merge($importedAliases, $localAliases),
1035: static fn (?TypeAlias $typeAlias): bool => $typeAlias !== null,
1036: );
1037: }
1038:
1039: return $this->typeAliases;
1040: }
1041:
1042: public function getDeprecatedDescription(): ?string
1043: {
1044: if ($this->deprecatedDescription === null && $this->isDeprecated()) {
1045: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1046: if ($resolvedPhpDoc !== null && $resolvedPhpDoc->getDeprecatedTag() !== null) {
1047: $this->deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag()->getMessage();
1048: }
1049: }
1050:
1051: return $this->deprecatedDescription;
1052: }
1053:
1054: public function isDeprecated(): bool
1055: {
1056: if ($this->isDeprecated === null) {
1057: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1058: $this->isDeprecated = $resolvedPhpDoc !== null && $resolvedPhpDoc->isDeprecated();
1059: }
1060:
1061: return $this->isDeprecated;
1062: }
1063:
1064: public function isBuiltin(): bool
1065: {
1066: return $this->reflection->isInternal();
1067: }
1068:
1069: public function isInternal(): bool
1070: {
1071: if ($this->isInternal === null) {
1072: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1073: $this->isInternal = $resolvedPhpDoc !== null && $resolvedPhpDoc->isInternal();
1074: }
1075:
1076: return $this->isInternal;
1077: }
1078:
1079: public function isFinal(): bool
1080: {
1081: if ($this->isFinalByKeyword()) {
1082: return true;
1083: }
1084:
1085: if ($this->isFinal === null) {
1086: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1087: $this->isFinal = $resolvedPhpDoc !== null && $resolvedPhpDoc->isFinal();
1088: }
1089:
1090: return $this->isFinal;
1091: }
1092:
1093: public function isImmutable(): bool
1094: {
1095: if ($this->isImmutable === null) {
1096: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1097: $this->isImmutable = $resolvedPhpDoc !== null && $resolvedPhpDoc->isImmutable();
1098:
1099: $parentClass = $this->getParentClass();
1100: if ($parentClass !== null && !$this->isImmutable) {
1101: $this->isImmutable = $parentClass->isImmutable();
1102: }
1103: }
1104:
1105: return $this->isImmutable;
1106: }
1107:
1108: public function hasConsistentConstructor(): bool
1109: {
1110: if ($this->hasConsistentConstructor === null) {
1111: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1112: $this->hasConsistentConstructor = $resolvedPhpDoc !== null && $resolvedPhpDoc->hasConsistentConstructor();
1113: }
1114:
1115: return $this->hasConsistentConstructor;
1116: }
1117:
1118: public function isFinalByKeyword(): bool
1119: {
1120: if ($this->isAnonymous()) {
1121: return true;
1122: }
1123:
1124: return $this->reflection->isFinal();
1125: }
1126:
1127: public function isAttributeClass(): bool
1128: {
1129: return $this->findAttributeFlags() !== null;
1130: }
1131:
1132: private function findAttributeFlags(): ?int
1133: {
1134: if ($this->isInterface() || $this->isTrait() || $this->isEnum()) {
1135: return null;
1136: }
1137:
1138: $nativeAttributes = $this->reflection->getAttributes(Attribute::class);
1139: if (count($nativeAttributes) === 1) {
1140: if (!$this->reflectionProvider->hasClass(Attribute::class)) {
1141: return null;
1142: }
1143:
1144: $attributeClass = $this->reflectionProvider->getClass(Attribute::class);
1145: $arguments = [];
1146: foreach ($nativeAttributes[0]->getArgumentsExpressions() as $i => $expression) {
1147: $arguments[] = new Arg($expression, false, false, [], is_int($i) ? null : new Identifier($i));
1148: }
1149:
1150: $attributeConstructor = $attributeClass->getConstructor();
1151: $attributeConstructorVariant = ParametersAcceptorSelector::selectSingle($attributeConstructor->getVariants());
1152:
1153: if (count($arguments) === 0) {
1154: $flagType = $attributeConstructorVariant->getParameters()[0]->getDefaultValue();
1155: } else {
1156: $staticCall = ArgumentsNormalizer::reorderStaticCallArguments(
1157: $attributeConstructorVariant,
1158: new StaticCall(new FullyQualified(Attribute::class), $attributeConstructor->getName(), $arguments),
1159: );
1160: if ($staticCall === null) {
1161: return null;
1162: }
1163: $flagExpr = $staticCall->getArgs()[0]->value;
1164: $flagType = $this->initializerExprTypeResolver->getType($flagExpr, InitializerExprContext::fromClassReflection($this));
1165: }
1166:
1167: if (!$flagType instanceof ConstantIntegerType) {
1168: return null;
1169: }
1170:
1171: return $flagType->getValue();
1172: }
1173:
1174: return null;
1175: }
1176:
1177: public function getAttributeClassFlags(): int
1178: {
1179: $flags = $this->findAttributeFlags();
1180: if ($flags === null) {
1181: throw new ShouldNotHappenException();
1182: }
1183:
1184: return $flags;
1185: }
1186:
1187: public function getTemplateTypeMap(): TemplateTypeMap
1188: {
1189: if ($this->templateTypeMap !== null) {
1190: return $this->templateTypeMap;
1191: }
1192:
1193: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1194: if ($resolvedPhpDoc === null) {
1195: $this->templateTypeMap = TemplateTypeMap::createEmpty();
1196: return $this->templateTypeMap;
1197: }
1198:
1199: $templateTypeScope = TemplateTypeScope::createWithClass($this->getName());
1200:
1201: $templateTypeMap = new TemplateTypeMap(array_map(static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag), $this->getTemplateTags()));
1202:
1203: $this->templateTypeMap = $templateTypeMap;
1204:
1205: return $templateTypeMap;
1206: }
1207:
1208: public function getActiveTemplateTypeMap(): TemplateTypeMap
1209: {
1210: if ($this->activeTemplateTypeMap !== null) {
1211: return $this->activeTemplateTypeMap;
1212: }
1213: $resolved = $this->resolvedTemplateTypeMap;
1214: if ($resolved !== null) {
1215: $templateTypeMap = $this->getTemplateTypeMap();
1216: return $this->activeTemplateTypeMap = $resolved->map(static function (string $name, Type $type) use ($templateTypeMap): Type {
1217: if ($type instanceof ErrorType) {
1218: $templateType = $templateTypeMap->getType($name);
1219: if ($templateType !== null) {
1220: return TemplateTypeHelper::resolveToBounds($templateType);
1221: }
1222: }
1223:
1224: return $type;
1225: });
1226: }
1227:
1228: return $this->activeTemplateTypeMap = $this->getTemplateTypeMap();
1229: }
1230:
1231: public function getPossiblyIncompleteActiveTemplateTypeMap(): TemplateTypeMap
1232: {
1233: return $this->resolvedTemplateTypeMap ?? $this->getTemplateTypeMap();
1234: }
1235:
1236: public function isGeneric(): bool
1237: {
1238: if ($this->isGeneric === null) {
1239: if ($this->isEnum()) {
1240: return $this->isGeneric = false;
1241: }
1242:
1243: $this->isGeneric = count($this->getTemplateTags()) > 0;
1244: }
1245:
1246: return $this->isGeneric;
1247: }
1248:
1249: /**
1250: * @param array<int, Type> $types
1251: */
1252: public function typeMapFromList(array $types): TemplateTypeMap
1253: {
1254: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1255: if ($resolvedPhpDoc === null) {
1256: return TemplateTypeMap::createEmpty();
1257: }
1258:
1259: $map = [];
1260: $i = 0;
1261: foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1262: $map[$tag->getName()] = $types[$i] ?? new ErrorType();
1263: $i++;
1264: }
1265:
1266: return new TemplateTypeMap($map);
1267: }
1268:
1269: /** @return array<int, Type> */
1270: public function typeMapToList(TemplateTypeMap $typeMap): array
1271: {
1272: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1273: if ($resolvedPhpDoc === null) {
1274: return [];
1275: }
1276:
1277: $list = [];
1278: foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
1279: $list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound();
1280: }
1281:
1282: return $list;
1283: }
1284:
1285: /**
1286: * @param array<int, Type> $types
1287: */
1288: public function withTypes(array $types): self
1289: {
1290: return new self(
1291: $this->reflectionProvider,
1292: $this->initializerExprTypeResolver,
1293: $this->fileTypeMapper,
1294: $this->stubPhpDocProvider,
1295: $this->phpDocInheritanceResolver,
1296: $this->phpVersion,
1297: $this->propertiesClassReflectionExtensions,
1298: $this->methodsClassReflectionExtensions,
1299: $this->allowedSubTypesClassReflectionExtensions,
1300: $this->displayName,
1301: $this->reflection,
1302: $this->anonymousFilename,
1303: $this->typeMapFromList($types),
1304: $this->stubPhpDocBlock,
1305: );
1306: }
1307:
1308: public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
1309: {
1310: if ($this->stubPhpDocBlock !== null) {
1311: return $this->stubPhpDocBlock;
1312: }
1313:
1314: $fileName = $this->getFileName();
1315: if (is_bool($this->reflectionDocComment)) {
1316: $docComment = $this->reflection->getDocComment();
1317: $this->reflectionDocComment = $docComment !== false ? $docComment : null;
1318: }
1319:
1320: if ($this->reflectionDocComment === null) {
1321: return null;
1322: }
1323:
1324: if ($this->resolvedPhpDocBlock !== false) {
1325: return $this->resolvedPhpDocBlock;
1326: }
1327:
1328: return $this->resolvedPhpDocBlock = $this->fileTypeMapper->getResolvedPhpDoc($fileName, $this->getName(), null, null, $this->reflectionDocComment);
1329: }
1330:
1331: private function getFirstExtendsTag(): ?ExtendsTag
1332: {
1333: foreach ($this->getExtendsTags() as $tag) {
1334: return $tag;
1335: }
1336:
1337: return null;
1338: }
1339:
1340: /** @return array<string, ExtendsTag> */
1341: public function getExtendsTags(): array
1342: {
1343: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1344: if ($resolvedPhpDoc === null) {
1345: return [];
1346: }
1347:
1348: return $resolvedPhpDoc->getExtendsTags();
1349: }
1350:
1351: /** @return array<string, ImplementsTag> */
1352: public function getImplementsTags(): array
1353: {
1354: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1355: if ($resolvedPhpDoc === null) {
1356: return [];
1357: }
1358:
1359: return $resolvedPhpDoc->getImplementsTags();
1360: }
1361:
1362: /** @return array<string,TemplateTag> */
1363: public function getTemplateTags(): array
1364: {
1365: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1366: if ($resolvedPhpDoc === null) {
1367: return [];
1368: }
1369:
1370: return $resolvedPhpDoc->getTemplateTags();
1371: }
1372:
1373: /**
1374: * @return array<string,ClassReflection>
1375: */
1376: public function getAncestors(): array
1377: {
1378: $ancestors = $this->ancestors;
1379:
1380: if ($ancestors === null) {
1381: $ancestors = [
1382: $this->getName() => $this,
1383: ];
1384:
1385: $addToAncestors = static function (string $name, ClassReflection $classReflection) use (&$ancestors): void {
1386: if (array_key_exists($name, $ancestors)) {
1387: return;
1388: }
1389:
1390: $ancestors[$name] = $classReflection;
1391: };
1392:
1393: foreach ($this->getInterfaces() as $interface) {
1394: $addToAncestors($interface->getName(), $interface);
1395: foreach ($interface->getAncestors() as $name => $ancestor) {
1396: $addToAncestors($name, $ancestor);
1397: }
1398: }
1399:
1400: foreach ($this->getTraits() as $trait) {
1401: $addToAncestors($trait->getName(), $trait);
1402: foreach ($trait->getAncestors() as $name => $ancestor) {
1403: $addToAncestors($name, $ancestor);
1404: }
1405: }
1406:
1407: $parent = $this->getParentClass();
1408: if ($parent !== null) {
1409: $addToAncestors($parent->getName(), $parent);
1410: foreach ($parent->getAncestors() as $name => $ancestor) {
1411: $addToAncestors($name, $ancestor);
1412: }
1413: }
1414:
1415: $this->ancestors = $ancestors;
1416: }
1417:
1418: return $ancestors;
1419: }
1420:
1421: public function getAncestorWithClassName(string $className): ?self
1422: {
1423: return $this->getAncestors()[$className] ?? null;
1424: }
1425:
1426: /**
1427: * @param string[] $ancestorClasses
1428: */
1429: private function isValidAncestorType(Type $type, array $ancestorClasses): bool
1430: {
1431: if (!$type instanceof GenericObjectType) {
1432: return false;
1433: }
1434:
1435: $reflection = $type->getClassReflection();
1436: if ($reflection === null) {
1437: return false;
1438: }
1439:
1440: return in_array($reflection->getName(), $ancestorClasses, true);
1441: }
1442:
1443: /**
1444: * @return array<MixinTag>
1445: */
1446: public function getMixinTags(): array
1447: {
1448: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1449: if ($resolvedPhpDoc === null) {
1450: return [];
1451: }
1452:
1453: return $resolvedPhpDoc->getMixinTags();
1454: }
1455:
1456: /**
1457: * @return array<PropertyTag>
1458: */
1459: public function getPropertyTags(): array
1460: {
1461: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1462: if ($resolvedPhpDoc === null) {
1463: return [];
1464: }
1465:
1466: return $resolvedPhpDoc->getPropertyTags();
1467: }
1468:
1469: /**
1470: * @return array<MethodTag>
1471: */
1472: public function getMethodTags(): array
1473: {
1474: $resolvedPhpDoc = $this->getResolvedPhpDoc();
1475: if ($resolvedPhpDoc === null) {
1476: return [];
1477: }
1478:
1479: return $resolvedPhpDoc->getMethodTags();
1480: }
1481:
1482: /**
1483: * @return array<Type>
1484: */
1485: public function getResolvedMixinTypes(): array
1486: {
1487: $types = [];
1488: foreach ($this->getMixinTags() as $mixinTag) {
1489: if (!$this->isGeneric()) {
1490: $types[] = $mixinTag->getType();
1491: continue;
1492: }
1493:
1494: $types[] = TemplateTypeHelper::resolveTemplateTypes(
1495: $mixinTag->getType(),
1496: $this->getActiveTemplateTypeMap(),
1497: );
1498: }
1499:
1500: return $types;
1501: }
1502:
1503: /**
1504: * @return array<Type>|null
1505: */
1506: public function getAllowedSubTypes(): ?array
1507: {
1508: foreach ($this->allowedSubTypesClassReflectionExtensions as $allowedSubTypesClassReflectionExtension) {
1509: if ($allowedSubTypesClassReflectionExtension->supports($this)) {
1510: return $allowedSubTypesClassReflectionExtension->getAllowedSubTypes($this);
1511: }
1512: }
1513:
1514: return null;
1515: }
1516:
1517: }
1518: