1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use ArrayAccess;
6: use Closure;
7: use Countable;
8: use DateTime;
9: use DateTimeImmutable;
10: use DateTimeInterface;
11: use Error;
12: use Exception;
13: use Iterator;
14: use IteratorAggregate;
15: use PHPStan\Analyser\OutOfClassScope;
16: use PHPStan\Broker\Broker;
17: use PHPStan\Broker\ClassNotFoundException;
18: use PHPStan\Reflection\ClassMemberAccessAnswerer;
19: use PHPStan\Reflection\ClassReflection;
20: use PHPStan\Reflection\ConstantReflection;
21: use PHPStan\Reflection\ExtendedMethodReflection;
22: use PHPStan\Reflection\ParametersAcceptor;
23: use PHPStan\Reflection\ParametersAcceptorSelector;
24: use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension;
25: use PHPStan\Reflection\PropertyReflection;
26: use PHPStan\Reflection\ReflectionProviderStaticAccessor;
27: use PHPStan\Reflection\TrivialParametersAcceptor;
28: use PHPStan\Reflection\Type\CalledOnTypeUnresolvedMethodPrototypeReflection;
29: use PHPStan\Reflection\Type\CalledOnTypeUnresolvedPropertyPrototypeReflection;
30: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
31: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
32: use PHPStan\ShouldNotHappenException;
33: use PHPStan\TrinaryLogic;
34: use PHPStan\Type\Constant\ConstantArrayType;
35: use PHPStan\Type\Constant\ConstantBooleanType;
36: use PHPStan\Type\Constant\ConstantStringType;
37: use PHPStan\Type\Enum\EnumCaseObjectType;
38: use PHPStan\Type\Generic\GenericObjectType;
39: use PHPStan\Type\Traits\MaybeIterableTypeTrait;
40: use PHPStan\Type\Traits\NonArrayTypeTrait;
41: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
42: use PHPStan\Type\Traits\NonGenericTypeTrait;
43: use PHPStan\Type\Traits\UndecidedComparisonTypeTrait;
44: use Throwable;
45: use Traversable;
46: use function array_key_exists;
47: use function array_map;
48: use function array_merge;
49: use function array_values;
50: use function count;
51: use function implode;
52: use function in_array;
53: use function sprintf;
54: use function strtolower;
55:
56: /** @api */
57: class ObjectType implements TypeWithClassName, SubtractableType
58: {
59:
60: use MaybeIterableTypeTrait;
61: use NonArrayTypeTrait;
62: use NonGenericTypeTrait;
63: use UndecidedComparisonTypeTrait;
64: use NonGeneralizableTypeTrait;
65:
66: private const EXTRA_OFFSET_CLASSES = ['SimpleXMLElement', 'DOMNodeList', 'Threaded'];
67:
68: private ?Type $subtractedType;
69:
70: /** @var array<string, array<string, TrinaryLogic>> */
71: private static array $superTypes = [];
72:
73: private ?self $cachedParent = null;
74:
75: /** @var self[]|null */
76: private ?array $cachedInterfaces = null;
77:
78: /** @var array<string, array<string, array<string, UnresolvedMethodPrototypeReflection>>> */
79: private static array $methods = [];
80:
81: /** @var array<string, array<string, array<string, UnresolvedPropertyPrototypeReflection>>> */
82: private static array $properties = [];
83:
84: /** @var array<string, array<string, self|null>> */
85: private static array $ancestors = [];
86:
87: /** @var array<string, self|null> */
88: private array $currentAncestors = [];
89:
90: private ?string $cachedDescription = null;
91:
92: /** @api */
93: public function __construct(
94: private string $className,
95: ?Type $subtractedType = null,
96: private ?ClassReflection $classReflection = null,
97: )
98: {
99: if ($subtractedType instanceof NeverType) {
100: $subtractedType = null;
101: }
102:
103: $this->subtractedType = $subtractedType;
104: }
105:
106: public static function resetCaches(): void
107: {
108: self::$superTypes = [];
109: self::$methods = [];
110: self::$properties = [];
111: self::$ancestors = [];
112: }
113:
114: private static function createFromReflection(ClassReflection $reflection): self
115: {
116: if (!$reflection->isGeneric()) {
117: return new ObjectType($reflection->getName());
118: }
119:
120: return new GenericObjectType(
121: $reflection->getName(),
122: $reflection->typeMapToList($reflection->getActiveTemplateTypeMap()),
123: );
124: }
125:
126: public function getClassName(): string
127: {
128: return $this->className;
129: }
130:
131: public function hasProperty(string $propertyName): TrinaryLogic
132: {
133: $classReflection = $this->getClassReflection();
134: if ($classReflection === null) {
135: return TrinaryLogic::createMaybe();
136: }
137:
138: if ($classReflection->hasProperty($propertyName)) {
139: return TrinaryLogic::createYes();
140: }
141:
142: if ($classReflection->allowsDynamicProperties()) {
143: return TrinaryLogic::createMaybe();
144: }
145:
146: return TrinaryLogic::createNo();
147: }
148:
149: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
150: {
151: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
152: }
153:
154: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
155: {
156: if (!$scope->isInClass()) {
157: $canAccessProperty = 'no';
158: } else {
159: $canAccessProperty = $scope->getClassReflection()->getName();
160: }
161: $description = $this->describeCache();
162:
163: if (isset(self::$properties[$description][$propertyName][$canAccessProperty])) {
164: return self::$properties[$description][$propertyName][$canAccessProperty];
165: }
166:
167: $nakedClassReflection = $this->getNakedClassReflection();
168: if ($nakedClassReflection === null) {
169: throw new ClassNotFoundException($this->className);
170: }
171:
172: if (!$nakedClassReflection->hasNativeProperty($propertyName)) {
173: $nakedClassReflection = $this->getClassReflection();
174: }
175:
176: if ($nakedClassReflection === null) {
177: throw new ClassNotFoundException($this->className);
178: }
179:
180: $property = $nakedClassReflection->getProperty($propertyName, $scope);
181:
182: $ancestor = $this->getAncestorWithClassName($property->getDeclaringClass()->getName());
183: $resolvedClassReflection = null;
184: if ($ancestor !== null && $ancestor->hasProperty($propertyName)->yes()) {
185: $resolvedClassReflection = $ancestor->getClassReflection();
186: if ($ancestor !== $this) {
187: $property = $ancestor->getUnresolvedPropertyPrototype($propertyName, $scope)->getNakedProperty();
188: }
189: }
190: if ($resolvedClassReflection === null) {
191: $resolvedClassReflection = $property->getDeclaringClass();
192: }
193:
194: return self::$properties[$description][$propertyName][$canAccessProperty] = new CalledOnTypeUnresolvedPropertyPrototypeReflection(
195: $property,
196: $resolvedClassReflection,
197: true,
198: $this,
199: );
200: }
201:
202: public function getPropertyWithoutTransformingStatic(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
203: {
204: $classReflection = $this->getNakedClassReflection();
205: if ($classReflection === null) {
206: throw new ClassNotFoundException($this->className);
207: }
208:
209: if (!$classReflection->hasProperty($propertyName)) {
210: $classReflection = $this->getClassReflection();
211: }
212:
213: if ($classReflection === null) {
214: throw new ClassNotFoundException($this->className);
215: }
216:
217: return $classReflection->getProperty($propertyName, $scope);
218: }
219:
220: /**
221: * @return string[]
222: */
223: public function getReferencedClasses(): array
224: {
225: return [$this->className];
226: }
227:
228: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
229: {
230: if ($type instanceof StaticType) {
231: return $this->checkSubclassAcceptability($type->getClassName());
232: }
233:
234: if ($type instanceof CompoundType) {
235: return $type->isAcceptedBy($this, $strictTypes);
236: }
237:
238: if ($type instanceof ClosureType) {
239: return $this->isInstanceOf(Closure::class);
240: }
241:
242: if ($type instanceof ObjectWithoutClassType) {
243: return TrinaryLogic::createMaybe();
244: }
245:
246: if (!$type instanceof TypeWithClassName) {
247: return TrinaryLogic::createNo();
248: }
249:
250: return $this->checkSubclassAcceptability($type->getClassName());
251: }
252:
253: public function isSuperTypeOf(Type $type): TrinaryLogic
254: {
255: if (!$type instanceof CompoundType && !$type instanceof TypeWithClassName && !$type instanceof ObjectWithoutClassType) {
256: return TrinaryLogic::createNo();
257: }
258:
259: $thisDescription = $this->describeCache();
260:
261: if ($type instanceof self) {
262: $description = $type->describeCache();
263: } else {
264: $description = $type->describe(VerbosityLevel::cache());
265: }
266:
267: if (isset(self::$superTypes[$thisDescription][$description])) {
268: return self::$superTypes[$thisDescription][$description];
269: }
270:
271: if ($type instanceof CompoundType) {
272: return self::$superTypes[$thisDescription][$description] = $type->isSubTypeOf($this);
273: }
274:
275: if ($type instanceof ObjectWithoutClassType) {
276: if ($type->getSubtractedType() !== null) {
277: $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this);
278: if ($isSuperType->yes()) {
279: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
280: }
281: }
282: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
283: }
284:
285: $transformResult = static fn (TrinaryLogic $result) => $result;
286: if ($this->subtractedType !== null) {
287: $isSuperType = $this->subtractedType->isSuperTypeOf($type);
288: if ($isSuperType->yes()) {
289: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
290: }
291: if ($isSuperType->maybe()) {
292: $transformResult = static fn (TrinaryLogic $result) => $result->and(TrinaryLogic::createMaybe());
293: }
294: }
295:
296: if (
297: $type instanceof SubtractableType
298: && $type->getSubtractedType() !== null
299: ) {
300: $isSuperType = $type->getSubtractedType()->isSuperTypeOf($this);
301: if ($isSuperType->yes()) {
302: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
303: }
304: }
305:
306: $thisClassName = $this->className;
307: $thatClassName = $type->getClassName();
308:
309: if ($thatClassName === $thisClassName) {
310: return $transformResult(TrinaryLogic::createYes());
311: }
312:
313: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
314:
315: if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClassName)) {
316: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
317: }
318:
319: $thisClassReflection = $this->getClassReflection();
320: $thatClassReflection = $reflectionProvider->getClass($thatClassName);
321:
322: if ($thisClassReflection->isTrait() || $thatClassReflection->isTrait()) {
323: return TrinaryLogic::createNo();
324: }
325:
326: if ($thisClassReflection->getName() === $thatClassReflection->getName()) {
327: return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes());
328: }
329:
330: if ($thatClassReflection->isSubclassOf($thisClassName)) {
331: return self::$superTypes[$thisDescription][$description] = $transformResult(TrinaryLogic::createYes());
332: }
333:
334: if ($thisClassReflection->isSubclassOf($thatClassName)) {
335: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
336: }
337:
338: if ($thisClassReflection->isInterface() && !$thatClassReflection->getNativeReflection()->isFinal()) {
339: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
340: }
341:
342: if ($thatClassReflection->isInterface() && !$thisClassReflection->getNativeReflection()->isFinal()) {
343: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
344: }
345:
346: return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
347: }
348:
349: public function equals(Type $type): bool
350: {
351: if (!$type instanceof self) {
352: return false;
353: }
354:
355: if ($type instanceof EnumCaseObjectType) {
356: return false;
357: }
358:
359: if ($this->className !== $type->className) {
360: return false;
361: }
362:
363: if ($this->subtractedType === null) {
364: if ($type->subtractedType === null) {
365: return true;
366: }
367:
368: return false;
369: }
370:
371: if ($type->subtractedType === null) {
372: return false;
373: }
374:
375: return $this->subtractedType->equals($type->subtractedType);
376: }
377:
378: private function checkSubclassAcceptability(string $thatClass): TrinaryLogic
379: {
380: if ($this->className === $thatClass) {
381: return TrinaryLogic::createYes();
382: }
383:
384: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
385:
386: if ($this->getClassReflection() === null || !$reflectionProvider->hasClass($thatClass)) {
387: return TrinaryLogic::createNo();
388: }
389:
390: $thisReflection = $this->getClassReflection();
391: $thatReflection = $reflectionProvider->getClass($thatClass);
392:
393: if ($thisReflection->getName() === $thatReflection->getName()) {
394: // class alias
395: return TrinaryLogic::createYes();
396: }
397:
398: if ($thisReflection->isInterface() && $thatReflection->isInterface()) {
399: return TrinaryLogic::createFromBoolean(
400: $thatReflection->implementsInterface($this->className),
401: );
402: }
403:
404: return TrinaryLogic::createFromBoolean(
405: $thatReflection->isSubclassOf($this->className),
406: );
407: }
408:
409: public function describe(VerbosityLevel $level): string
410: {
411: $preciseNameCallback = function (): string {
412: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
413: if (!$reflectionProvider->hasClass($this->className)) {
414: return $this->className;
415: }
416:
417: return $reflectionProvider->getClassName($this->className);
418: };
419:
420: $preciseWithSubtracted = function () use ($level): string {
421: $description = $this->className;
422: if ($this->subtractedType !== null) {
423: $description .= sprintf('~%s', $this->subtractedType->describe($level));
424: }
425:
426: return $description;
427: };
428:
429: return $level->handle(
430: $preciseNameCallback,
431: $preciseNameCallback,
432: $preciseWithSubtracted,
433: function () use ($preciseWithSubtracted): string {
434: $reflection = $this->classReflection;
435: $line = '';
436: if ($reflection !== null) {
437: $line .= '-';
438: $line .= (string) $reflection->getNativeReflection()->getStartLine();
439: $line .= '-';
440: }
441:
442: return $preciseWithSubtracted() . '-' . static::class . '-' . $line . $this->describeAdditionalCacheKey();
443: },
444: );
445: }
446:
447: protected function describeAdditionalCacheKey(): string
448: {
449: return '';
450: }
451:
452: private function describeCache(): string
453: {
454: if ($this->cachedDescription !== null) {
455: return $this->cachedDescription;
456: }
457:
458: if (static::class !== self::class) {
459: return $this->cachedDescription = $this->describe(VerbosityLevel::cache());
460: }
461:
462: $description = $this->className;
463:
464: if ($this instanceof GenericObjectType) {
465: $description .= '<';
466: $typeDescriptions = [];
467: foreach ($this->getTypes() as $type) {
468: $typeDescriptions[] = $type->describe(VerbosityLevel::cache());
469: }
470: $description .= '<' . implode(', ', $typeDescriptions) . '>';
471: }
472:
473: if ($this->subtractedType !== null) {
474: $description .= sprintf('~%s', $this->subtractedType->describe(VerbosityLevel::cache()));
475: }
476:
477: $reflection = $this->classReflection;
478: if ($reflection !== null) {
479: $description .= '-';
480: $description .= (string) $reflection->getNativeReflection()->getStartLine();
481: $description .= '-';
482: }
483:
484: return $this->cachedDescription = $description;
485: }
486:
487: public function toNumber(): Type
488: {
489: if ($this->isInstanceOf('SimpleXMLElement')->yes()) {
490: return new UnionType([
491: new FloatType(),
492: new IntegerType(),
493: ]);
494: }
495:
496: return new ErrorType();
497: }
498:
499: public function toInteger(): Type
500: {
501: if ($this->isInstanceOf('SimpleXMLElement')->yes()) {
502: return new IntegerType();
503: }
504:
505: if (in_array($this->getClassName(), ['CurlHandle', 'CurlMultiHandle'], true)) {
506: return new IntegerType();
507: }
508:
509: return new ErrorType();
510: }
511:
512: public function toFloat(): Type
513: {
514: if ($this->isInstanceOf('SimpleXMLElement')->yes()) {
515: return new FloatType();
516: }
517: return new ErrorType();
518: }
519:
520: public function toString(): Type
521: {
522: $classReflection = $this->getClassReflection();
523: if ($classReflection === null) {
524: return new ErrorType();
525: }
526:
527: if ($classReflection->hasNativeMethod('__toString')) {
528: return ParametersAcceptorSelector::selectSingle($this->getMethod('__toString', new OutOfClassScope())->getVariants())->getReturnType();
529: }
530:
531: return new ErrorType();
532: }
533:
534: public function toArray(): Type
535: {
536: $classReflection = $this->getClassReflection();
537: if ($classReflection === null) {
538: return new ArrayType(new MixedType(), new MixedType());
539: }
540:
541: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
542:
543: if (
544: !$classReflection->getNativeReflection()->isUserDefined()
545: || UniversalObjectCratesClassReflectionExtension::isUniversalObjectCrate(
546: $reflectionProvider,
547: Broker::getInstance()->getUniversalObjectCratesClasses(),
548: $classReflection,
549: )
550: ) {
551: return new ArrayType(new MixedType(), new MixedType());
552: }
553: $arrayKeys = [];
554: $arrayValues = [];
555:
556: $isFinal = $classReflection->isFinal();
557:
558: do {
559: foreach ($classReflection->getNativeReflection()->getProperties() as $nativeProperty) {
560: if ($nativeProperty->isStatic()) {
561: continue;
562: }
563:
564: $declaringClass = $reflectionProvider->getClass($nativeProperty->getDeclaringClass()->getName());
565: $property = $declaringClass->getNativeProperty($nativeProperty->getName());
566:
567: $keyName = $nativeProperty->getName();
568: if ($nativeProperty->isPrivate()) {
569: $keyName = sprintf(
570: "\0%s\0%s",
571: $declaringClass->getName(),
572: $keyName,
573: );
574: } elseif ($nativeProperty->isProtected()) {
575: $keyName = sprintf(
576: "\0*\0%s",
577: $keyName,
578: );
579: }
580:
581: $arrayKeys[] = new ConstantStringType($keyName);
582: $arrayValues[] = $property->getReadableType();
583: }
584:
585: $classReflection = $classReflection->getParentClass();
586: } while ($classReflection !== null);
587:
588: if (!$isFinal && count($arrayKeys) === 0) {
589: return new ArrayType(new MixedType(), new MixedType());
590: }
591:
592: return new ConstantArrayType($arrayKeys, $arrayValues);
593: }
594:
595: public function toArrayKey(): Type
596: {
597: return $this->toString();
598: }
599:
600: public function toBoolean(): BooleanType
601: {
602: if ($this->isInstanceOf('SimpleXMLElement')->yes()) {
603: return new BooleanType();
604: }
605:
606: return new ConstantBooleanType(true);
607: }
608:
609: public function canAccessProperties(): TrinaryLogic
610: {
611: return TrinaryLogic::createYes();
612: }
613:
614: public function canCallMethods(): TrinaryLogic
615: {
616: if (strtolower($this->className) === 'stdclass') {
617: return TrinaryLogic::createNo();
618: }
619:
620: return TrinaryLogic::createYes();
621: }
622:
623: public function hasMethod(string $methodName): TrinaryLogic
624: {
625: $classReflection = $this->getClassReflection();
626: if ($classReflection === null) {
627: return TrinaryLogic::createMaybe();
628: }
629:
630: if ($classReflection->hasMethod($methodName)) {
631: return TrinaryLogic::createYes();
632: }
633:
634: if ($classReflection->isFinal()) {
635: return TrinaryLogic::createNo();
636: }
637:
638: return TrinaryLogic::createMaybe();
639: }
640:
641: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
642: {
643: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
644: }
645:
646: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
647: {
648: if (!$scope->isInClass()) {
649: $canCallMethod = 'no';
650: } else {
651: $canCallMethod = $scope->getClassReflection()->getName();
652: }
653: $description = $this->describeCache();
654: if (isset(self::$methods[$description][$methodName][$canCallMethod])) {
655: return self::$methods[$description][$methodName][$canCallMethod];
656: }
657:
658: $nakedClassReflection = $this->getNakedClassReflection();
659: if ($nakedClassReflection === null) {
660: throw new ClassNotFoundException($this->className);
661: }
662:
663: if (!$nakedClassReflection->hasNativeMethod($methodName)) {
664: $nakedClassReflection = $this->getClassReflection();
665: }
666:
667: if ($nakedClassReflection === null) {
668: throw new ClassNotFoundException($this->className);
669: }
670:
671: $method = $nakedClassReflection->getMethod($methodName, $scope);
672:
673: $ancestor = $this->getAncestorWithClassName($method->getDeclaringClass()->getName());
674: $resolvedClassReflection = null;
675: if ($ancestor !== null) {
676: $resolvedClassReflection = $ancestor->getClassReflection();
677: if ($ancestor !== $this) {
678: $method = $ancestor->getUnresolvedMethodPrototype($methodName, $scope)->getNakedMethod();
679: }
680: }
681: if ($resolvedClassReflection === null) {
682: $resolvedClassReflection = $method->getDeclaringClass();
683: }
684:
685: return self::$methods[$description][$methodName][$canCallMethod] = new CalledOnTypeUnresolvedMethodPrototypeReflection(
686: $method,
687: $resolvedClassReflection,
688: true,
689: $this,
690: );
691: }
692:
693: public function canAccessConstants(): TrinaryLogic
694: {
695: return TrinaryLogic::createYes();
696: }
697:
698: public function hasConstant(string $constantName): TrinaryLogic
699: {
700: $class = $this->getClassReflection();
701: if ($class === null) {
702: return TrinaryLogic::createNo();
703: }
704:
705: return TrinaryLogic::createFromBoolean(
706: $class->hasConstant($constantName),
707: );
708: }
709:
710: public function getConstant(string $constantName): ConstantReflection
711: {
712: $class = $this->getClassReflection();
713: if ($class === null) {
714: throw new ClassNotFoundException($this->className);
715: }
716:
717: return $class->getConstant($constantName);
718: }
719:
720: public function getConstantStrings(): array
721: {
722: return [];
723: }
724:
725: public function isIterable(): TrinaryLogic
726: {
727: return $this->isInstanceOf(Traversable::class);
728: }
729:
730: public function isIterableAtLeastOnce(): TrinaryLogic
731: {
732: return $this->isInstanceOf(Traversable::class)
733: ->and(TrinaryLogic::createMaybe());
734: }
735:
736: public function getArraySize(): Type
737: {
738: if ($this->isInstanceOf(Countable::class)->no()) {
739: return new ErrorType();
740: }
741:
742: return IntegerRangeType::fromInterval(0, null);
743: }
744:
745: public function getIterableKeyType(): Type
746: {
747: $isTraversable = false;
748: if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
749: $keyType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
750: $this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
751: )->getReturnType()->getIterableKeyType());
752: $isTraversable = true;
753: if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) {
754: return $keyType;
755: }
756: }
757:
758: $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes();
759: if ($this->isInstanceOf(Traversable::class)->yes() && !$extraOffsetAccessible) {
760: $isTraversable = true;
761: $tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey');
762: if ($tKey !== null) {
763: if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) {
764: return $tKey;
765: }
766: }
767: }
768:
769: if ($this->isInstanceOf(Iterator::class)->yes()) {
770: return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
771: $this->getMethod('key', new OutOfClassScope())->getVariants(),
772: )->getReturnType());
773: }
774:
775: if ($extraOffsetAccessible) {
776: return new MixedType(true);
777: }
778:
779: if ($isTraversable) {
780: return new MixedType();
781: }
782:
783: return new ErrorType();
784: }
785:
786: public function getFirstIterableKeyType(): Type
787: {
788: return $this->getIterableKeyType();
789: }
790:
791: public function getLastIterableKeyType(): Type
792: {
793: return $this->getIterableKeyType();
794: }
795:
796: public function getIterableValueType(): Type
797: {
798: $isTraversable = false;
799: if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
800: $valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
801: $this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
802: )->getReturnType()->getIterableValueType());
803: $isTraversable = true;
804: if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) {
805: return $valueType;
806: }
807: }
808:
809: $extraOffsetAccessible = $this->isExtraOffsetAccessibleClass()->yes();
810: if ($this->isInstanceOf(Traversable::class)->yes() && !$extraOffsetAccessible) {
811: $isTraversable = true;
812: $tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue');
813: if ($tValue !== null) {
814: if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) {
815: return $tValue;
816: }
817: }
818: }
819:
820: if ($this->isInstanceOf(Iterator::class)->yes()) {
821: return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
822: $this->getMethod('current', new OutOfClassScope())->getVariants(),
823: )->getReturnType());
824: }
825:
826: if ($extraOffsetAccessible) {
827: return new MixedType(true);
828: }
829:
830: if ($isTraversable) {
831: return new MixedType();
832: }
833:
834: return new ErrorType();
835: }
836:
837: public function getFirstIterableValueType(): Type
838: {
839: return $this->getIterableValueType();
840: }
841:
842: public function getLastIterableValueType(): Type
843: {
844: return $this->getIterableValueType();
845: }
846:
847: public function isNull(): TrinaryLogic
848: {
849: return TrinaryLogic::createNo();
850: }
851:
852: public function isTrue(): TrinaryLogic
853: {
854: return TrinaryLogic::createNo();
855: }
856:
857: public function isFalse(): TrinaryLogic
858: {
859: return TrinaryLogic::createNo();
860: }
861:
862: public function isBoolean(): TrinaryLogic
863: {
864: return TrinaryLogic::createNo();
865: }
866:
867: public function isFloat(): TrinaryLogic
868: {
869: return TrinaryLogic::createNo();
870: }
871:
872: public function isInteger(): TrinaryLogic
873: {
874: return TrinaryLogic::createNo();
875: }
876:
877: public function isString(): TrinaryLogic
878: {
879: return TrinaryLogic::createNo();
880: }
881:
882: public function isNumericString(): TrinaryLogic
883: {
884: return TrinaryLogic::createNo();
885: }
886:
887: public function isNonEmptyString(): TrinaryLogic
888: {
889: return TrinaryLogic::createNo();
890: }
891:
892: public function isNonFalsyString(): TrinaryLogic
893: {
894: return TrinaryLogic::createNo();
895: }
896:
897: public function isLiteralString(): TrinaryLogic
898: {
899: return TrinaryLogic::createNo();
900: }
901:
902: public function isClassStringType(): TrinaryLogic
903: {
904: return TrinaryLogic::createNo();
905: }
906:
907: public function isVoid(): TrinaryLogic
908: {
909: return TrinaryLogic::createNo();
910: }
911:
912: public function isScalar(): TrinaryLogic
913: {
914: return TrinaryLogic::createNo();
915: }
916:
917: private function isExtraOffsetAccessibleClass(): TrinaryLogic
918: {
919: $classReflection = $this->getClassReflection();
920: if ($classReflection === null) {
921: return TrinaryLogic::createMaybe();
922: }
923:
924: foreach (self::EXTRA_OFFSET_CLASSES as $extraOffsetClass) {
925: if ($classReflection->getName() === $extraOffsetClass) {
926: return TrinaryLogic::createYes();
927: }
928: if ($classReflection->isSubclassOf($extraOffsetClass)) {
929: return TrinaryLogic::createYes();
930: }
931: }
932:
933: if ($classReflection->isInterface()) {
934: return TrinaryLogic::createMaybe();
935: }
936:
937: if ($classReflection->isFinal()) {
938: return TrinaryLogic::createNo();
939: }
940:
941: return TrinaryLogic::createMaybe();
942: }
943:
944: public function isOffsetAccessible(): TrinaryLogic
945: {
946: return $this->isInstanceOf(ArrayAccess::class)->or(
947: $this->isExtraOffsetAccessibleClass(),
948: );
949: }
950:
951: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
952: {
953: if ($this->isInstanceOf(ArrayAccess::class)->yes()) {
954: $acceptedOffsetType = RecursionGuard::run($this, function (): Type {
955: $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters();
956: if (count($parameters) < 2) {
957: throw new ShouldNotHappenException(sprintf(
958: 'Method %s::%s() has less than 2 parameters.',
959: $this->className,
960: 'offsetSet',
961: ));
962: }
963:
964: $offsetParameter = $parameters[0];
965:
966: return $offsetParameter->getType();
967: });
968:
969: if ($acceptedOffsetType->isSuperTypeOf($offsetType)->no()) {
970: return TrinaryLogic::createNo();
971: }
972:
973: return TrinaryLogic::createMaybe();
974: }
975:
976: return $this->isExtraOffsetAccessibleClass()
977: ->and(TrinaryLogic::createMaybe());
978: }
979:
980: public function getOffsetValueType(Type $offsetType): Type
981: {
982: if (!$this->isExtraOffsetAccessibleClass()->no()) {
983: return new MixedType();
984: }
985:
986: if ($this->isInstanceOf(ArrayAccess::class)->yes()) {
987: return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle($this->getMethod('offsetGet', new OutOfClassScope())->getVariants())->getReturnType());
988: }
989:
990: return new ErrorType();
991: }
992:
993: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
994: {
995: if ($this->isOffsetAccessible()->no()) {
996: return new ErrorType();
997: }
998:
999: if ($this->isInstanceOf(ArrayAccess::class)->yes()) {
1000: $acceptedValueType = new NeverType();
1001: $acceptedOffsetType = RecursionGuard::run($this, function () use (&$acceptedValueType): Type {
1002: $parameters = ParametersAcceptorSelector::selectSingle($this->getMethod('offsetSet', new OutOfClassScope())->getVariants())->getParameters();
1003: if (count($parameters) < 2) {
1004: throw new ShouldNotHappenException(sprintf(
1005: 'Method %s::%s() has less than 2 parameters.',
1006: $this->className,
1007: 'offsetSet',
1008: ));
1009: }
1010:
1011: $offsetParameter = $parameters[0];
1012: $acceptedValueType = $parameters[1]->getType();
1013:
1014: return $offsetParameter->getType();
1015: });
1016:
1017: if ($offsetType === null) {
1018: $offsetType = new NullType();
1019: }
1020:
1021: if (
1022: (!$offsetType instanceof MixedType && !$acceptedOffsetType->isSuperTypeOf($offsetType)->yes())
1023: || (!$valueType instanceof MixedType && !$acceptedValueType->isSuperTypeOf($valueType)->yes())
1024: ) {
1025: return new ErrorType();
1026: }
1027: }
1028:
1029: // in the future we may return intersection of $this and OffsetAccessibleType()
1030: return $this;
1031: }
1032:
1033: public function unsetOffset(Type $offsetType): Type
1034: {
1035: if ($this->isOffsetAccessible()->no()) {
1036: return new ErrorType();
1037: }
1038:
1039: return $this;
1040: }
1041:
1042: public function isCallable(): TrinaryLogic
1043: {
1044: $parametersAcceptors = $this->findCallableParametersAcceptors();
1045: if ($parametersAcceptors === null) {
1046: return TrinaryLogic::createNo();
1047: }
1048:
1049: if (
1050: count($parametersAcceptors) === 1
1051: && $parametersAcceptors[0] instanceof TrivialParametersAcceptor
1052: ) {
1053: return TrinaryLogic::createMaybe();
1054: }
1055:
1056: return TrinaryLogic::createYes();
1057: }
1058:
1059: /**
1060: * @return ParametersAcceptor[]
1061: */
1062: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1063: {
1064: if ($this->className === Closure::class) {
1065: return [new TrivialParametersAcceptor()];
1066: }
1067: $parametersAcceptors = $this->findCallableParametersAcceptors();
1068: if ($parametersAcceptors === null) {
1069: throw new ShouldNotHappenException();
1070: }
1071:
1072: return $parametersAcceptors;
1073: }
1074:
1075: /**
1076: * @return ParametersAcceptor[]|null
1077: */
1078: private function findCallableParametersAcceptors(): ?array
1079: {
1080: $classReflection = $this->getClassReflection();
1081: if ($classReflection === null) {
1082: return [new TrivialParametersAcceptor()];
1083: }
1084:
1085: if ($classReflection->hasNativeMethod('__invoke')) {
1086: return $this->getMethod('__invoke', new OutOfClassScope())->getVariants();
1087: }
1088:
1089: if (!$classReflection->getNativeReflection()->isFinal()) {
1090: return [new TrivialParametersAcceptor()];
1091: }
1092:
1093: return null;
1094: }
1095:
1096: public function isCloneable(): TrinaryLogic
1097: {
1098: return TrinaryLogic::createYes();
1099: }
1100:
1101: /**
1102: * @param mixed[] $properties
1103: */
1104: public static function __set_state(array $properties): Type
1105: {
1106: return new self(
1107: $properties['className'],
1108: $properties['subtractedType'] ?? null,
1109: );
1110: }
1111:
1112: public function isInstanceOf(string $className): TrinaryLogic
1113: {
1114: $classReflection = $this->getClassReflection();
1115: if ($classReflection === null) {
1116: return TrinaryLogic::createMaybe();
1117: }
1118:
1119: if ($classReflection->isSubclassOf($className) || $classReflection->getName() === $className) {
1120: return TrinaryLogic::createYes();
1121: }
1122:
1123: if ($classReflection->isInterface()) {
1124: return TrinaryLogic::createMaybe();
1125: }
1126:
1127: return TrinaryLogic::createNo();
1128: }
1129:
1130: public function subtract(Type $type): Type
1131: {
1132: if ($this->subtractedType !== null) {
1133: $type = TypeCombinator::union($this->subtractedType, $type);
1134: }
1135:
1136: return $this->changeSubtractedType($type);
1137: }
1138:
1139: public function getTypeWithoutSubtractedType(): Type
1140: {
1141: return $this->changeSubtractedType(null);
1142: }
1143:
1144: public function changeSubtractedType(?Type $subtractedType): Type
1145: {
1146: if ($subtractedType !== null) {
1147: $classReflection = $this->getClassReflection();
1148: $allowedSubTypesList = $classReflection !== null ? $classReflection->getAllowedSubTypes() : null;
1149: if ($allowedSubTypesList !== null) {
1150: $allowedSubTypes = [];
1151: foreach ($allowedSubTypesList as $allowedSubType) {
1152: $allowedSubTypes[$allowedSubType->describe(VerbosityLevel::precise())] = $allowedSubType;
1153: }
1154:
1155: $originalAllowedSubTypes = $allowedSubTypes;
1156: $subtractedSubTypes = [];
1157:
1158: $subtractedTypesList = TypeUtils::flattenTypes($subtractedType);
1159: if ($this->subtractedType !== null) {
1160: $subtractedTypesList = array_merge($subtractedTypesList, TypeUtils::flattenTypes($this->subtractedType));
1161: }
1162:
1163: $subtractedTypes = [];
1164: foreach ($subtractedTypesList as $type) {
1165: $subtractedTypes[$type->describe(VerbosityLevel::precise())] = $type;
1166: }
1167:
1168: foreach ($subtractedTypes as $subType) {
1169: foreach ($allowedSubTypes as $description => $allowedSubType) {
1170: if ($subType->equals($allowedSubType)) {
1171: $subtractedSubTypes[$description] = $subType;
1172: unset($allowedSubTypes[$description]);
1173: continue 2;
1174: }
1175: }
1176:
1177: return new self($this->className, $subtractedType);
1178: }
1179:
1180: if (count($allowedSubTypes) === 1) {
1181: return array_values($allowedSubTypes)[0];
1182: }
1183:
1184: $subtractedSubTypes = array_values($subtractedSubTypes);
1185: $subtractedSubTypesCount = count($subtractedSubTypes);
1186: if ($subtractedSubTypesCount === count($originalAllowedSubTypes)) {
1187: return new NeverType();
1188: }
1189:
1190: if ($subtractedSubTypesCount === 0) {
1191: return new self($this->className);
1192: }
1193:
1194: if ($subtractedSubTypesCount === 1) {
1195: return new self($this->className, $subtractedSubTypes[0]);
1196: }
1197:
1198: return new self($this->className, new UnionType($subtractedSubTypes));
1199: }
1200: }
1201:
1202: if ($this->subtractedType === null && $subtractedType === null) {
1203: return $this;
1204: }
1205:
1206: return new self($this->className, $subtractedType);
1207: }
1208:
1209: public function getSubtractedType(): ?Type
1210: {
1211: return $this->subtractedType;
1212: }
1213:
1214: public function traverse(callable $cb): Type
1215: {
1216: $subtractedType = $this->subtractedType !== null ? $cb($this->subtractedType) : null;
1217:
1218: if ($subtractedType !== $this->subtractedType) {
1219: return new self(
1220: $this->className,
1221: $subtractedType,
1222: );
1223: }
1224:
1225: return $this;
1226: }
1227:
1228: public function getNakedClassReflection(): ?ClassReflection
1229: {
1230: if ($this->classReflection !== null) {
1231: return $this->classReflection;
1232: }
1233:
1234: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
1235: if (!$reflectionProvider->hasClass($this->className)) {
1236: return null;
1237: }
1238:
1239: return $reflectionProvider->getClass($this->className);
1240: }
1241:
1242: public function getClassReflection(): ?ClassReflection
1243: {
1244: if ($this->classReflection !== null) {
1245: return $this->classReflection;
1246: }
1247:
1248: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
1249: if (!$reflectionProvider->hasClass($this->className)) {
1250: return null;
1251: }
1252:
1253: $classReflection = $reflectionProvider->getClass($this->className);
1254: if ($classReflection->isGeneric()) {
1255: return $classReflection->withTypes(array_values($classReflection->getTemplateTypeMap()->map(static fn (): Type => new ErrorType())->getTypes()));
1256: }
1257:
1258: return $classReflection;
1259: }
1260:
1261: /**
1262: * @return self|null
1263: */
1264: public function getAncestorWithClassName(string $className): ?TypeWithClassName
1265: {
1266: if ($this->className === $className) {
1267: return $this;
1268: }
1269:
1270: if ($this->classReflection !== null && $className === $this->classReflection->getName()) {
1271: return $this;
1272: }
1273:
1274: if (array_key_exists($className, $this->currentAncestors)) {
1275: return $this->currentAncestors[$className];
1276: }
1277:
1278: $description = $this->describeCache();
1279: if (
1280: array_key_exists($description, self::$ancestors)
1281: && array_key_exists($className, self::$ancestors[$description])
1282: ) {
1283: return self::$ancestors[$description][$className];
1284: }
1285:
1286: $reflectionProvider = ReflectionProviderStaticAccessor::getInstance();
1287: if (!$reflectionProvider->hasClass($className)) {
1288: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null;
1289: }
1290: $theirReflection = $reflectionProvider->getClass($className);
1291:
1292: $thisReflection = $this->getClassReflection();
1293: if ($thisReflection === null) {
1294: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null;
1295: }
1296: if ($theirReflection->getName() === $thisReflection->getName()) {
1297: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $this;
1298: }
1299:
1300: foreach ($this->getInterfaces() as $interface) {
1301: $ancestor = $interface->getAncestorWithClassName($className);
1302: if ($ancestor !== null) {
1303: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $ancestor;
1304: }
1305: }
1306:
1307: $parent = $this->getParent();
1308: if ($parent !== null) {
1309: $ancestor = $parent->getAncestorWithClassName($className);
1310: if ($ancestor !== null) {
1311: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = $ancestor;
1312: }
1313: }
1314:
1315: return self::$ancestors[$description][$className] = $this->currentAncestors[$className] = null;
1316: }
1317:
1318: private function getParent(): ?ObjectType
1319: {
1320: if ($this->cachedParent !== null) {
1321: return $this->cachedParent;
1322: }
1323: $thisReflection = $this->getClassReflection();
1324: if ($thisReflection === null) {
1325: return null;
1326: }
1327:
1328: $parentReflection = $thisReflection->getParentClass();
1329: if ($parentReflection === null) {
1330: return null;
1331: }
1332:
1333: return $this->cachedParent = self::createFromReflection($parentReflection);
1334: }
1335:
1336: /** @return ObjectType[] */
1337: private function getInterfaces(): array
1338: {
1339: if ($this->cachedInterfaces !== null) {
1340: return $this->cachedInterfaces;
1341: }
1342: $thisReflection = $this->getClassReflection();
1343: if ($thisReflection === null) {
1344: return $this->cachedInterfaces = [];
1345: }
1346:
1347: return $this->cachedInterfaces = array_map(static fn (ClassReflection $interfaceReflection): self => self::createFromReflection($interfaceReflection), $thisReflection->getInterfaces());
1348: }
1349:
1350: public function tryRemove(Type $typeToRemove): ?Type
1351: {
1352: if ($this->getClassName() === DateTimeInterface::class) {
1353: if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTimeImmutable::class) {
1354: return new ObjectType(DateTime::class);
1355: }
1356:
1357: if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === DateTime::class) {
1358: return new ObjectType(DateTimeImmutable::class);
1359: }
1360: }
1361:
1362: if ($this->getClassName() === Throwable::class) {
1363: if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Error::class) {
1364: return new ObjectType(Exception::class); // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException
1365: }
1366:
1367: if ($typeToRemove instanceof ObjectType && $typeToRemove->getClassName() === Exception::class) { // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException
1368: return new ObjectType(Error::class);
1369: }
1370: }
1371:
1372: if ($this->isSuperTypeOf($typeToRemove)->yes()) {
1373: return $this->subtract($typeToRemove);
1374: }
1375:
1376: return null;
1377: }
1378:
1379: }
1380: