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