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