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