1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use DateTime;
6: use DateTimeImmutable;
7: use DateTimeInterface;
8: use Error;
9: use Exception;
10: use PHPStan\DependencyInjection\ReportUnsafeArrayStringKeyCastingToggle;
11: use PHPStan\Php\PhpVersion;
12: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
13: use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
14: use PHPStan\Reflection\ClassConstantReflection;
15: use PHPStan\Reflection\ClassMemberAccessAnswerer;
16: use PHPStan\Reflection\ExtendedMethodReflection;
17: use PHPStan\Reflection\ExtendedPropertyReflection;
18: use PHPStan\Reflection\InitializerExprTypeResolver;
19: use PHPStan\Reflection\MissingMethodFromReflectionException;
20: use PHPStan\Reflection\MissingPropertyFromReflectionException;
21: use PHPStan\Reflection\ReflectionProvider;
22: use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection;
23: use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection;
24: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
25: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
26: use PHPStan\ShouldNotHappenException;
27: use PHPStan\TrinaryLogic;
28: use PHPStan\Type\Enum\EnumCaseObjectType;
29: use PHPStan\Type\Generic\GenericClassStringType;
30: use PHPStan\Type\Generic\TemplateIterableType;
31: use PHPStan\Type\Generic\TemplateMixedType;
32: use PHPStan\Type\Generic\TemplateType;
33: use PHPStan\Type\Generic\TemplateTypeMap;
34: use PHPStan\Type\Generic\TemplateTypeVariance;
35: use PHPStan\Type\Generic\TemplateUnionType;
36: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
37: use Throwable;
38: use function array_diff_assoc;
39: use function array_fill_keys;
40: use function array_intersect;
41: use function array_keys;
42: use function array_map;
43: use function array_merge;
44: use function array_slice;
45: use function array_unique;
46: use function array_values;
47: use function count;
48: use function implode;
49: use function sprintf;
50: use function str_contains;
51:
52: /** @api */
53: class UnionType implements CompoundType
54: {
55:
56: use NonGeneralizableTypeTrait;
57:
58: public const EQUAL_UNION_CLASSES = [
59: DateTimeInterface::class => [DateTimeImmutable::class, DateTime::class],
60: Throwable::class => [Error::class, Exception::class], // phpcs:ignore SlevomatCodingStandard.Exceptions.ReferenceThrowableOnly.ReferencedGeneralException
61: ];
62:
63: /**
64: * Sorting must not reorder $types: describe() sorts for readability, but $types is the
65: * type's value — getTypes() exposes it and callers merge array shapes in that order. It
66: * used to be sorted in place, so describing a union permanently changed what getTypes()
67: * returned, making an immutable value object's observable state depend on what had been
68: * called on it before.
69: *
70: * @var list<Type>|null
71: */
72: private ?array $sortedTypesCache = null;
73:
74: /** @var array<int, string> */
75: private array $cachedDescriptions = [];
76:
77: /**
78: * Identity-keyed view of $types, built on first use. False once it is known that the
79: * members cannot be keyed at all, so a union of objects pays for the attempt only once.
80: */
81: private FiniteTypeSet|false|null $finiteTypeSet = null;
82:
83: /** @var list<Type>|null */
84: private ?array $finiteTypes = null;
85:
86: private ?TrinaryLogic $isNull = null;
87:
88: private ?TrinaryLogic $isCallable = null;
89:
90: /**
91: * @api
92: * @param list<Type> $types
93: */
94: public function __construct(private array $types, private bool $normalized = false)
95: {
96: $throwException = static function () use ($types): void {
97: throw new ShouldNotHappenException(sprintf(
98: 'Cannot create %s with: %s',
99: self::class,
100: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
101: ));
102: };
103: if (count($types) < 2) {
104: $throwException();
105: }
106: foreach ($types as $type) {
107: if (!($type instanceof UnionType)) {
108: continue;
109: }
110: if ($type instanceof TemplateType) {
111: continue;
112: }
113:
114: $throwException();
115: }
116: }
117:
118: /**
119: * @return list<Type>
120: */
121: public function getTypes(): array
122: {
123: return $this->types;
124: }
125:
126: /**
127: * @param callable(Type $type): bool $filterCb
128: */
129: public function filterTypes(callable $filterCb): Type
130: {
131: $newTypes = [];
132: $changed = false;
133: foreach ($this->getTypes() as $innerType) {
134: if (!$filterCb($innerType)) {
135: $changed = true;
136: continue;
137: }
138:
139: $newTypes[] = $innerType;
140: }
141:
142: if (!$changed) {
143: return $this;
144: }
145:
146: return TypeCombinator::union(...$newTypes);
147: }
148:
149: public function isNormalized(): bool
150: {
151: return $this->normalized;
152: }
153:
154: public function getFiniteTypeSet(): ?FiniteTypeSet
155: {
156: $finiteTypeSet = $this->finiteTypeSet ??= FiniteTypeSet::create($this->types) ?? false;
157: if ($finiteTypeSet === false) {
158: return null;
159: }
160:
161: return $finiteTypeSet;
162: }
163:
164: /**
165: * @return list<Type>
166: */
167: protected function getSortedTypes(): array
168: {
169: return $this->sortedTypesCache ??= UnionTypeHelper::sortTypes($this->types);
170: }
171:
172: public function getReferencedClasses(): array
173: {
174: $classes = [];
175: foreach ($this->types as $type) {
176: foreach ($type->getReferencedClasses() as $className) {
177: $classes[] = $className;
178: }
179: }
180:
181: return $classes;
182: }
183:
184: public function getObjectClassNames(): array
185: {
186: return array_values(array_unique($this->pickFromTypes(
187: static fn (Type $type) => $type->getObjectClassNames(),
188: static fn (Type $type) => $type->isObject()->yes(),
189: )));
190: }
191:
192: public function getObjectClassReflections(): array
193: {
194: return $this->pickFromTypes(
195: static fn (Type $type) => $type->getObjectClassReflections(),
196: static fn (Type $type) => $type->isObject()->yes(),
197: );
198: }
199:
200: public function getArrays(): array
201: {
202: return $this->pickFromTypes(
203: static fn (Type $type) => $type->getArrays(),
204: static fn (Type $type) => $type->isArray()->yes(),
205: );
206: }
207:
208: public function getConstantArrays(): array
209: {
210: return $this->pickFromTypes(
211: static fn (Type $type) => $type->getConstantArrays(),
212: static fn (Type $type) => $type->isArray()->yes(),
213: );
214: }
215:
216: public function getConstantStrings(): array
217: {
218: return $this->pickFromTypes(
219: static fn (Type $type) => $type->getConstantStrings(),
220: static fn (Type $type) => $type->isString()->yes(),
221: );
222: }
223:
224: public function accepts(Type $type, bool $strictTypes): AcceptsResult
225: {
226: $finiteTypeSet = $this->getFiniteTypeSet();
227: if ($finiteTypeSet !== null) {
228: $key = FiniteTypeSet::key($type);
229: if ($key !== null && $finiteTypeSet->has($key)) {
230: return AcceptsResult::createYes();
231: }
232:
233: if ($finiteTypeSet->isComplete()) {
234: if ($key !== null) {
235: // A value the union does not hold can still be accepted through scalar
236: // coercion, so unlike isSuperTypeOf() the answer is not simply no - but it
237: // is the same for every member of a kind, so one member of each is enough.
238: // None of the branches below apply to a value: it is not iterable, not
239: // compound, and an enum case already is the union of its own cases.
240: $result = AcceptsResult::createNo();
241: foreach ($finiteTypeSet->getRepresentativesOfOtherKinds($type) as $representative) {
242: $result = $result->or($representative->accepts($type, $strictTypes));
243: }
244:
245: return $result;
246: }
247:
248: if ($type instanceof self && !$type instanceof TemplateType) {
249: $otherFiniteTypeSet = $type->getFiniteTypeSet();
250: if ($otherFiniteTypeSet !== null && $otherFiniteTypeSet->isComplete()) {
251: // A member standing for a single value never accepts more than one of
252: // the other union's values, and the other union has at least two of
253: // them - so the member-by-member or() below cannot come out yes, and
254: // its result is discarded in favour of the compound answer anyway.
255: return $type->isAcceptedBy($this, $strictTypes);
256: }
257: }
258: }
259: }
260:
261: if ($type instanceof IterableType) {
262: return $this->accepts($type->toArrayOrTraversable(), $strictTypes);
263: }
264:
265: foreach (self::EQUAL_UNION_CLASSES as $baseClass => $classes) {
266: if (!$type->equals(new ObjectType($baseClass))) {
267: continue;
268: }
269:
270: $union = TypeCombinator::union(
271: ...array_map(static fn (string $objectClass): Type => new ObjectType($objectClass), $classes),
272: );
273: if ($this->accepts($union, $strictTypes)->yes()) {
274: return AcceptsResult::createYes();
275: }
276: break;
277: }
278:
279: $innerAccepts = [];
280: $result = AcceptsResult::createNo();
281: foreach ($this->getSortedTypes() as $i => $innerType) {
282: $innerResult = $innerType->accepts($type, $strictTypes);
283: $innerAccepts[$i] = $innerResult;
284: $result = $result->or($innerResult->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason)));
285: }
286: if ($result->yes()) {
287: return $result;
288: }
289:
290: $commonReasons = null;
291: foreach ($innerAccepts as $innerResult) {
292: if ($commonReasons === null) {
293: $commonReasons = $innerResult->reasons;
294: continue;
295: }
296: $commonReasons = array_values(array_intersect($commonReasons, $innerResult->reasons));
297: }
298: if ($commonReasons !== null && count($commonReasons) > 0) {
299: $decorated = [];
300: foreach (array_keys($innerAccepts) as $i) {
301: foreach ($commonReasons as $reason) {
302: $decorated[] = sprintf('Type #%d from the union: %s', $i + 1, $reason);
303: }
304: }
305: $result = new AcceptsResult($result->result, $decorated);
306: }
307:
308: if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) {
309: return $type->isAcceptedBy($this, $strictTypes);
310: }
311:
312: if ($type instanceof TemplateUnionType) {
313: return $result->or($type->isAcceptedBy($this, $strictTypes));
314: }
315:
316: if ($type->isEnum()->yes() && !$this->isEnum()->no()) {
317: $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases());
318: if (!$type->equals($enumCasesUnion)) {
319: return $this->accepts($enumCasesUnion, $strictTypes);
320: }
321: }
322:
323: return $result;
324: }
325:
326: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
327: {
328: if (
329: ($otherType instanceof self && !$otherType instanceof TemplateUnionType)
330: || ($otherType instanceof IterableType && !$otherType instanceof TemplateIterableType)
331: || $otherType instanceof NeverType
332: || $otherType instanceof IntegerRangeType
333: ) {
334: return $otherType->isSubTypeOf($this);
335: }
336:
337: $types = $this->types;
338: $finiteTypeSet = $this->getFiniteTypeSet();
339: if ($finiteTypeSet !== null) {
340: $key = FiniteTypeSet::key($otherType);
341: if ($key !== null) {
342: if ($finiteTypeSet->has($key)) {
343: return IsSuperTypeOfResult::createYes();
344: }
345:
346: // Every keyed member stands for a different value than $otherType, so all of
347: // them answer no - only the members that could not be keyed are left to ask.
348: if ($finiteTypeSet->isComplete()) {
349: return IsSuperTypeOfResult::createNo();
350: }
351:
352: $types = $finiteTypeSet->getOthers();
353: }
354: }
355:
356: $results = [];
357: foreach ($types as $innerType) {
358: $result = $innerType->isSuperTypeOf($otherType);
359: if ($result->yes()) {
360: return $result;
361: }
362: $results[] = $result;
363: }
364: $result = IsSuperTypeOfResult::createNo()->or(...$results);
365:
366: if (
367: $otherType instanceof TemplateUnionType
368: || ($otherType instanceof LateResolvableType && $otherType instanceof CompoundType && !$otherType instanceof TemplateType)
369: ) {
370: return $result->or($otherType->isSubTypeOf($this));
371: }
372:
373: return $result;
374: }
375:
376: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
377: {
378: $containment = $this->finiteTypeSetContainedIn($otherType, false);
379: if ($containment !== null) {
380: if ($containment->maybe()) {
381: return IsSuperTypeOfResult::createMaybe();
382: }
383:
384: return IsSuperTypeOfResult::createFromBoolean($containment->yes());
385: }
386:
387: return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types));
388: }
389:
390: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
391: {
392: // Unlike isSubTypeOf() only the positive answer holds: a member $acceptingType does
393: // not hold can still be accepted through scalar coercion.
394: $containment = $this->finiteTypeSetContainedIn($acceptingType, true);
395: if ($containment !== null && $containment->yes()) {
396: return AcceptsResult::createYes();
397: }
398:
399: return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types));
400: }
401:
402: /**
403: * Whether $otherType holds every member of this one, or null when the question cannot
404: * be settled from the identity maps alone.
405: *
406: * $otherType is compared as a set of values: a union through its own map, a single
407: * finite value as the one-member set holding it.
408: *
409: * $yesOnly skips the completeness requirement on the other union: a member missing from
410: * its map only rules out the yes answer, which is all the caller is after.
411: */
412: private function finiteTypeSetContainedIn(Type $otherType, bool $yesOnly): ?TrinaryLogic
413: {
414: $finiteTypeSet = $this->getFiniteTypeSet();
415: if ($finiteTypeSet === null || !$finiteTypeSet->isComplete()) {
416: return null;
417: }
418:
419: if (!$otherType instanceof self || $otherType instanceof TemplateType) {
420: // A single value covers a member iff they are the same value, and no member
421: // stands for a value it is not keyed by - so one lookup answers for every member
422: // at once. This is the shape TypeCombinator::remove() takes to ask whether
423: // removing a value empties a union of values, and the only way a non-union other
424: // type reaches this helper at all.
425: $key = FiniteTypeSet::key($otherType);
426: if ($key === null) {
427: return null;
428: }
429:
430: $containment = $finiteTypeSet->containedInKey($key);
431:
432: // No constant value accepts another one - not even a numeric string an int, in
433: // either direction - so today accepts() agrees with isSuperTypeOf() here and this
434: // guard never changes an answer. It is kept because that is a fact about the
435: // current value types rather than something the identity map guarantees: a value
436: // the map does not hold being accepted anyway is exactly what accepts() is
437: // allowed to do, and all isAcceptedBy() wants from the map is the yes.
438: if (!$containment->yes() && $yesOnly) {
439: return null;
440: }
441:
442: return $containment;
443: }
444:
445: $otherFiniteTypeSet = $otherType->getFiniteTypeSet();
446: if ($otherFiniteTypeSet === null) {
447: return null;
448: }
449:
450: $containment = $finiteTypeSet->containedIn($otherFiniteTypeSet);
451: if (!$containment->yes() && ($yesOnly || !$otherFiniteTypeSet->isComplete())) {
452: return null;
453: }
454:
455: return $containment;
456: }
457:
458: public function equals(Type $type): bool
459: {
460: if (!$type instanceof static) {
461: return false;
462: }
463:
464: if (count($this->types) !== count($type->types)) {
465: return false;
466: }
467:
468: $finiteTypeSet = $this->getFiniteTypeSet();
469: if ($finiteTypeSet !== null && $finiteTypeSet->isComplete()) {
470: $otherFiniteTypeSet = $type->getFiniteTypeSet();
471: if ($otherFiniteTypeSet !== null && $otherFiniteTypeSet->isComplete()) {
472: return $finiteTypeSet->containedIn($otherFiniteTypeSet)->yes();
473: }
474: }
475:
476: $otherTypes = $type->types;
477: foreach ($this->types as $innerType) {
478: $match = false;
479: foreach ($otherTypes as $i => $otherType) {
480: if (!$innerType->equals($otherType)) {
481: continue;
482: }
483:
484: $match = true;
485: unset($otherTypes[$i]);
486: break;
487: }
488:
489: if (!$match) {
490: return false;
491: }
492: }
493:
494: return count($otherTypes) === 0;
495: }
496:
497: public function describe(VerbosityLevel $level): string
498: {
499: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
500: return $this->cachedDescriptions[$level->getLevelValue()];
501: }
502: $joinTypes = static function (array $types) use ($level): string {
503: $typeNames = [];
504: foreach ($types as $i => $type) {
505: if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) {
506: $typeNames[] = sprintf('(%s)', $type->describe($level));
507: } elseif ($type instanceof TemplateType) {
508: $isLast = $i >= count($types) - 1;
509: $bound = $type->getBound();
510: if (
511: !$isLast
512: && ($level->isTypeOnly() || $level->isValue())
513: && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType)
514: ) {
515: $typeNames[] = sprintf('(%s)', $type->describe($level));
516: } else {
517: $typeNames[] = $type->describe($level);
518: }
519: } elseif ($type instanceof IntersectionType) {
520: $intersectionDescription = $type->describe($level);
521: if (str_contains($intersectionDescription, '&')) {
522: $typeNames[] = sprintf('(%s)', $intersectionDescription);
523: } else {
524: $typeNames[] = $intersectionDescription;
525: }
526: } else {
527: $typeNames[] = $type->describe($level);
528: }
529: }
530:
531: if ($level->isPrecise() || $level->isCache()) {
532: $duplicates = array_diff_assoc($typeNames, array_unique($typeNames));
533: if (count($duplicates) > 0) {
534: $indexByDuplicate = array_fill_keys($duplicates, 0);
535: foreach ($typeNames as $key => $typeName) {
536: if (!isset($indexByDuplicate[$typeName])) {
537: continue;
538: }
539:
540: $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName];
541: }
542: }
543: } else {
544: $typeNames = array_unique($typeNames);
545: }
546:
547: if (count($typeNames) > 1024) {
548: return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}";
549: }
550:
551: return implode('|', $typeNames);
552: };
553:
554: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
555: function () use ($joinTypes): string {
556: $types = TypeCombinator::union(...array_map(static function (Type $type): Type {
557: if (
558: $type->isConstantValue()->yes()
559: && $type->isTrue()->or($type->isFalse())->no()
560: ) {
561: return $type->generalize(GeneralizePrecision::lessSpecific());
562: }
563:
564: return $type;
565: }, $this->getSortedTypes()));
566:
567: if ($types instanceof UnionType) {
568: return $joinTypes($types->getSortedTypes());
569: }
570:
571: return $joinTypes([$types]);
572: },
573: fn (): string => $joinTypes($this->getSortedTypes()),
574: );
575: }
576:
577: /**
578: * @param callable(Type $type): TrinaryLogic $canCallback
579: * @param callable(Type $type): TrinaryLogic $hasCallback
580: */
581: private function hasInternal(
582: callable $canCallback,
583: callable $hasCallback,
584: ): TrinaryLogic
585: {
586: return TrinaryLogic::lazyExtremeIdentity($this->types, static function (Type $type) use ($canCallback, $hasCallback): TrinaryLogic {
587: if ($canCallback($type)->no()) {
588: return TrinaryLogic::createNo();
589: }
590:
591: return $hasCallback($type);
592: });
593: }
594:
595: /**
596: * @template TObject of object
597: * @param callable(Type $type): TrinaryLogic $hasCallback
598: * @param callable(Type $type): TObject $getCallback
599: * @return TObject
600: */
601: private function getInternal(
602: callable $hasCallback,
603: callable $getCallback,
604: ): object
605: {
606: /** @var TrinaryLogic|null $result */
607: $result = null;
608:
609: /** @var TObject|null $object */
610: $object = null;
611: foreach ($this->types as $type) {
612: $has = $hasCallback($type);
613: if (!$has->yes()) {
614: continue;
615: }
616: if ($result !== null && $result->compareTo($has) !== $has) {
617: continue;
618: }
619:
620: $get = $getCallback($type);
621: $result = $has;
622: $object = $get;
623: }
624:
625: if ($object === null) {
626: throw new ShouldNotHappenException();
627: }
628:
629: return $object;
630: }
631:
632: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
633: {
634: return $this->unionTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
635: }
636:
637: public function isObject(): TrinaryLogic
638: {
639: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject());
640: }
641:
642: public function getClassStringType(): Type
643: {
644: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringType());
645: }
646:
647: public function isEnum(): TrinaryLogic
648: {
649: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
650: }
651:
652: public function canAccessProperties(): TrinaryLogic
653: {
654: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
655: }
656:
657: public function hasProperty(string $propertyName): TrinaryLogic
658: {
659: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
660: }
661:
662: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
663: {
664: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
665: }
666:
667: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
668: {
669: $propertyPrototypes = [];
670: foreach ($this->types as $type) {
671: if (!$type->hasProperty($propertyName)->yes()) {
672: continue;
673: }
674:
675: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
676: }
677:
678: $propertiesCount = count($propertyPrototypes);
679: if ($propertiesCount === 0) {
680: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
681: }
682:
683: if ($propertiesCount === 1) {
684: return $propertyPrototypes[0];
685: }
686:
687: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
688: }
689:
690: public function hasInstanceProperty(string $propertyName): TrinaryLogic
691: {
692: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasInstanceProperty($propertyName));
693: }
694:
695: public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
696: {
697: return $this->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->getTransformedProperty();
698: }
699:
700: public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
701: {
702: $propertyPrototypes = [];
703: foreach ($this->types as $type) {
704: if (!$type->hasInstanceProperty($propertyName)->yes()) {
705: continue;
706: }
707:
708: $propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
709: }
710:
711: $propertiesCount = count($propertyPrototypes);
712: if ($propertiesCount === 0) {
713: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
714: }
715:
716: if ($propertiesCount === 1) {
717: return $propertyPrototypes[0];
718: }
719:
720: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
721: }
722:
723: public function hasStaticProperty(string $propertyName): TrinaryLogic
724: {
725: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasStaticProperty($propertyName));
726: }
727:
728: public function getStaticProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
729: {
730: return $this->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->getTransformedProperty();
731: }
732:
733: public function getUnresolvedStaticPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
734: {
735: $propertyPrototypes = [];
736: foreach ($this->types as $type) {
737: if (!$type->hasStaticProperty($propertyName)->yes()) {
738: continue;
739: }
740:
741: $propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
742: }
743:
744: $propertiesCount = count($propertyPrototypes);
745: if ($propertiesCount === 0) {
746: throw new MissingPropertyFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $propertyName);
747: }
748:
749: if ($propertiesCount === 1) {
750: return $propertyPrototypes[0];
751: }
752:
753: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyPrototypes);
754: }
755:
756: public function canCallMethods(): TrinaryLogic
757: {
758: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
759: }
760:
761: public function hasMethod(string $methodName): TrinaryLogic
762: {
763: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
764: }
765:
766: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
767: {
768: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
769: }
770:
771: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
772: {
773: $methodPrototypes = [];
774: foreach ($this->types as $type) {
775: if (!$type->hasMethod($methodName)->yes()) {
776: continue;
777: }
778:
779: $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope);
780: if ($this instanceof TemplateType) {
781: $prototype = $prototype->withCalledOnType($this);
782: }
783: $methodPrototypes[] = $prototype;
784: }
785:
786: $methodsCount = count($methodPrototypes);
787: if ($methodsCount === 0) {
788: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
789: }
790:
791: if ($methodsCount === 1) {
792: return $methodPrototypes[0];
793: }
794:
795: return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
796: }
797:
798: public function canAccessConstants(): TrinaryLogic
799: {
800: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
801: }
802:
803: public function hasConstant(string $constantName): TrinaryLogic
804: {
805: return $this->hasInternal(
806: static fn (Type $type): TrinaryLogic => $type->canAccessConstants(),
807: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
808: );
809: }
810:
811: public function getConstant(string $constantName): ClassConstantReflection
812: {
813: return $this->getInternal(
814: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
815: static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName),
816: );
817: }
818:
819: public function isIterable(): TrinaryLogic
820: {
821: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
822: }
823:
824: public function isIterableAtLeastOnce(): TrinaryLogic
825: {
826: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
827: }
828:
829: public function getArraySize(): Type
830: {
831: return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize());
832: }
833:
834: public function getIterableKeyType(): Type
835: {
836: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
837: }
838:
839: public function getFirstIterableKeyType(): Type
840: {
841: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
842: }
843:
844: public function getLastIterableKeyType(): Type
845: {
846: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
847: }
848:
849: public function getIterableValueType(): Type
850: {
851: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
852: }
853:
854: public function getFirstIterableValueType(): Type
855: {
856: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
857: }
858:
859: public function getLastIterableValueType(): Type
860: {
861: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
862: }
863:
864: public function isArray(): TrinaryLogic
865: {
866: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray());
867: }
868:
869: public function isConstantArray(): TrinaryLogic
870: {
871: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
872: }
873:
874: public function isOversizedArray(): TrinaryLogic
875: {
876: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
877: }
878:
879: public function isList(): TrinaryLogic
880: {
881: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList());
882: }
883:
884: public function isString(): TrinaryLogic
885: {
886: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString());
887: }
888:
889: public function isNumericString(): TrinaryLogic
890: {
891: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
892: }
893:
894: public function isDecimalIntegerString(): TrinaryLogic
895: {
896: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
897: }
898:
899: public function isNonEmptyString(): TrinaryLogic
900: {
901: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
902: }
903:
904: public function isNonFalsyString(): TrinaryLogic
905: {
906: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
907: }
908:
909: public function isLiteralString(): TrinaryLogic
910: {
911: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
912: }
913:
914: public function isLowercaseString(): TrinaryLogic
915: {
916: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
917: }
918:
919: public function isUppercaseString(): TrinaryLogic
920: {
921: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
922: }
923:
924: public function isClassString(): TrinaryLogic
925: {
926: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
927: }
928:
929: public function getClassStringObjectType(): Type
930: {
931: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
932: }
933:
934: public function getObjectTypeOrClassStringObjectType(): Type
935: {
936: return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
937: }
938:
939: public function isVoid(): TrinaryLogic
940: {
941: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
942: }
943:
944: public function isScalar(): TrinaryLogic
945: {
946: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
947: }
948:
949: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
950: {
951: return $this->notBenevolentUnionResults(
952: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
953: )->toBooleanType();
954: }
955:
956: public function isOffsetAccessible(): TrinaryLogic
957: {
958: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
959: }
960:
961: public function isOffsetAccessLegal(): TrinaryLogic
962: {
963: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
964: }
965:
966: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
967: {
968: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
969: }
970:
971: public function getOffsetValueType(Type $offsetType): Type
972: {
973: $types = [];
974: foreach ($this->types as $innerType) {
975: $valueType = $innerType->getOffsetValueType($offsetType);
976: if ($valueType instanceof ErrorType) {
977: continue;
978: }
979:
980: $types[] = $valueType;
981: }
982:
983: if (count($types) === 0) {
984: return new ErrorType();
985: }
986:
987: return TypeCombinator::union(...$types);
988: }
989:
990: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
991: {
992: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
993: }
994:
995: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
996: {
997: return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
998: }
999:
1000: public function unsetOffset(Type $offsetType): Type
1001: {
1002: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
1003: }
1004:
1005: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
1006: {
1007: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
1008: }
1009:
1010: public function getKeysArray(): Type
1011: {
1012: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray());
1013: }
1014:
1015: public function getValuesArray(): Type
1016: {
1017: return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
1018: }
1019:
1020: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
1021: {
1022: return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
1023: }
1024:
1025: public function fillKeysArray(Type $valueType): Type
1026: {
1027: return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
1028: }
1029:
1030: public function flipArray(): Type
1031: {
1032: return $this->unionTypes(static fn (Type $type): Type => $type->flipArray());
1033: }
1034:
1035: public function intersectKeyArray(Type $otherArraysType): Type
1036: {
1037: return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
1038: }
1039:
1040: public function popArray(): Type
1041: {
1042: return $this->unionTypes(static fn (Type $type): Type => $type->popArray());
1043: }
1044:
1045: public function reverseArray(TrinaryLogic $preserveKeys): Type
1046: {
1047: return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
1048: }
1049:
1050: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
1051: {
1052: return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
1053: }
1054:
1055: public function shiftArray(): Type
1056: {
1057: return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray());
1058: }
1059:
1060: public function shuffleArray(): Type
1061: {
1062: return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray());
1063: }
1064:
1065: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
1066: {
1067: return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
1068: }
1069:
1070: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1071: {
1072: return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
1073: }
1074:
1075: public function truncateListToSize(Type $sizeType): Type
1076: {
1077: return $this->unionTypes(static fn (Type $type): Type => $type->truncateListToSize($sizeType));
1078: }
1079:
1080: public function makeListMaybe(): Type
1081: {
1082: return $this->unionTypes(static fn (Type $type): Type => $type->makeListMaybe());
1083: }
1084:
1085: public function mapValueType(callable $cb): Type
1086: {
1087: return $this->unionTypes(static fn (Type $type): Type => $type->mapValueType($cb));
1088: }
1089:
1090: public function mapKeyType(callable $cb): Type
1091: {
1092: return $this->unionTypes(static fn (Type $type): Type => $type->mapKeyType($cb));
1093: }
1094:
1095: public function makeAllArrayKeysOptional(): Type
1096: {
1097: return $this->unionTypes(static fn (Type $type): Type => $type->makeAllArrayKeysOptional());
1098: }
1099:
1100: public function changeKeyCaseArray(?int $case): Type
1101: {
1102: return $this->unionTypes(static fn (Type $type): Type => $type->changeKeyCaseArray($case));
1103: }
1104:
1105: public function filterArrayRemovingFalsey(): Type
1106: {
1107: return $this->unionTypes(static fn (Type $type): Type => $type->filterArrayRemovingFalsey());
1108: }
1109:
1110: public function getEnumCases(): array
1111: {
1112: return $this->pickFromTypes(
1113: static fn (Type $type) => $type->getEnumCases(),
1114: static fn (Type $type) => $type->isObject()->yes(),
1115: );
1116: }
1117:
1118: public function getEnumCaseObject(): ?EnumCaseObjectType
1119: {
1120: $cases = $this->getEnumCases();
1121:
1122: if (count($cases) === 1) {
1123: return $cases[0];
1124: }
1125:
1126: return null;
1127: }
1128:
1129: public function isCallable(): TrinaryLogic
1130: {
1131: return $this->isCallable ??= $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
1132: }
1133:
1134: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
1135: {
1136: $acceptors = [];
1137:
1138: foreach ($this->types as $type) {
1139: if ($type->isCallable()->no()) {
1140: continue;
1141: }
1142:
1143: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
1144: }
1145:
1146: if (count($acceptors) === 0) {
1147: throw new ShouldNotHappenException();
1148: }
1149:
1150: return $acceptors;
1151: }
1152:
1153: public function isCloneable(): TrinaryLogic
1154: {
1155: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
1156: }
1157:
1158: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1159: {
1160: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
1161: }
1162:
1163: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1164: {
1165: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
1166: }
1167:
1168: public function isNull(): TrinaryLogic
1169: {
1170: return $this->isNull ??= $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
1171: }
1172:
1173: public function isConstantValue(): TrinaryLogic
1174: {
1175: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
1176: }
1177:
1178: public function isConstantScalarValue(): TrinaryLogic
1179: {
1180: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
1181: }
1182:
1183: public function getConstantScalarTypes(): array
1184: {
1185: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
1186: }
1187:
1188: public function getConstantScalarValues(): array
1189: {
1190: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
1191: }
1192:
1193: public function isTrue(): TrinaryLogic
1194: {
1195: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
1196: }
1197:
1198: public function isFalse(): TrinaryLogic
1199: {
1200: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
1201: }
1202:
1203: public function isBoolean(): TrinaryLogic
1204: {
1205: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
1206: }
1207:
1208: public function isFloat(): TrinaryLogic
1209: {
1210: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
1211: }
1212:
1213: public function isInteger(): TrinaryLogic
1214: {
1215: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
1216: }
1217:
1218: public function getSmallerType(PhpVersion $phpVersion): Type
1219: {
1220: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1221: }
1222:
1223: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1224: {
1225: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1226: }
1227:
1228: public function getGreaterType(PhpVersion $phpVersion): Type
1229: {
1230: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1231: }
1232:
1233: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1234: {
1235: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1236: }
1237:
1238: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1239: {
1240: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1241: }
1242:
1243: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1244: {
1245: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1246: }
1247:
1248: public function toBoolean(): BooleanType
1249: {
1250: /** @var BooleanType $type */
1251: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1252:
1253: return $type;
1254: }
1255:
1256: public function toNumber(): Type
1257: {
1258: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
1259:
1260: return $type;
1261: }
1262:
1263: public function toBitwiseNotType(): Type
1264: {
1265: return $this->unionTypes(static fn (Type $type): Type => $type->toBitwiseNotType());
1266: }
1267:
1268: public function toGetClassResultType(): Type
1269: {
1270: return $this->unionTypes(static fn (Type $type): Type => $type->toGetClassResultType());
1271: }
1272:
1273: public function toClassConstantType(ReflectionProvider $reflectionProvider): Type
1274: {
1275: return $this->unionTypes(static fn (Type $type): Type => $type->toClassConstantType($reflectionProvider));
1276: }
1277:
1278: public function toObjectTypeForInstanceofCheck(): ClassNameToObjectTypeResult
1279: {
1280: $types = [];
1281: $uncertainty = false;
1282: foreach ($this->getTypes() as $innerType) {
1283: $result = $innerType->toObjectTypeForInstanceofCheck();
1284: $types[] = $result->type;
1285: if (!$result->uncertainty) {
1286: continue;
1287: }
1288:
1289: $uncertainty = true;
1290: }
1291:
1292: return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty);
1293: }
1294:
1295: public function toObjectTypeForIsACheck(Type $objectOrClassType, bool $allowString, bool $allowSameClass): ClassNameToObjectTypeResult
1296: {
1297: $types = [];
1298: $uncertainty = false;
1299: foreach ($this->getTypes() as $innerType) {
1300: $result = $innerType->toObjectTypeForIsACheck($objectOrClassType, $allowString, $allowSameClass);
1301: $types[] = $result->type;
1302: if (!$result->uncertainty) {
1303: continue;
1304: }
1305:
1306: $uncertainty = true;
1307: }
1308:
1309: return new ClassNameToObjectTypeResult(TypeCombinator::union(...$types), $uncertainty);
1310: }
1311:
1312: public function toAbsoluteNumber(): Type
1313: {
1314: $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1315:
1316: return $type;
1317: }
1318:
1319: public function toString(): Type
1320: {
1321: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
1322:
1323: return $type;
1324: }
1325:
1326: public function toInteger(): Type
1327: {
1328: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
1329:
1330: return $type;
1331: }
1332:
1333: public function toFloat(): Type
1334: {
1335: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
1336:
1337: return $type;
1338: }
1339:
1340: public function toArray(): Type
1341: {
1342: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
1343:
1344: return $type;
1345: }
1346:
1347: public function toArrayKey(): Type
1348: {
1349: $level = ReportUnsafeArrayStringKeyCastingToggle::getLevel();
1350: if ($level !== ReportUnsafeArrayStringKeyCastingToggle::PREVENT || $this->isInteger()->no()) {
1351: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
1352: }
1353:
1354: return $this->unionTypes(static function (Type $type): Type {
1355: if ($type instanceof StringType) { // @phpstan-ignore phpstanApi.instanceofType
1356: return $type;
1357: }
1358:
1359: return $type->toArrayKey();
1360: });
1361: }
1362:
1363: public function toCoercedArgumentType(bool $strictTypes): Type
1364: {
1365: return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1366: }
1367:
1368: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1369: {
1370: if ($receivedType instanceof IterableType) {
1371: $receivedType = $receivedType->toArrayOrTraversable();
1372: }
1373:
1374: $types = TemplateTypeMap::createEmpty();
1375: if ($receivedType instanceof UnionType) {
1376: $myTypes = [];
1377: $remainingReceivedTypes = [];
1378: foreach ($receivedType->getTypes() as $receivedInnerType) {
1379: foreach ($this->types as $type) {
1380: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
1381: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
1382: continue 2;
1383: }
1384: $myTypes[] = $type;
1385: }
1386: $remainingReceivedTypes[] = $receivedInnerType;
1387: }
1388: if (count($remainingReceivedTypes) === 0) {
1389: return $types;
1390: }
1391: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
1392: } else {
1393: $myTypes = $this->types;
1394: }
1395:
1396: foreach ($myTypes as $type) {
1397: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
1398: continue;
1399: }
1400: $types = $types->union($type->inferTemplateTypes($receivedType));
1401: }
1402:
1403: if (!$types->isEmpty()) {
1404: return $types;
1405: }
1406:
1407: foreach ($myTypes as $type) {
1408: $types = $types->union($type->inferTemplateTypes($receivedType));
1409: }
1410:
1411: return $types;
1412: }
1413:
1414: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
1415: {
1416: $types = TemplateTypeMap::createEmpty();
1417:
1418: foreach ($this->types as $type) {
1419: $types = $types->union($templateType->inferTemplateTypes($type));
1420: }
1421:
1422: return $types;
1423: }
1424:
1425: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1426: {
1427: $references = [];
1428:
1429: foreach ($this->types as $type) {
1430: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1431: $references[] = $reference;
1432: }
1433: }
1434:
1435: return $references;
1436: }
1437:
1438: public function traverse(callable $cb): Type
1439: {
1440: $types = [];
1441: $changed = false;
1442:
1443: foreach ($this->types as $type) {
1444: $newType = $cb($type);
1445: if ($type !== $newType) {
1446: $changed = true;
1447: }
1448: $types[] = $newType;
1449: }
1450:
1451: if ($changed) {
1452: return TypeCombinator::union(...$types);
1453: }
1454:
1455: return $this;
1456: }
1457:
1458: public function traverseSimultaneously(Type $right, callable $cb): Type
1459: {
1460: $rightTypes = TypeUtils::flattenTypes($right);
1461: $newTypes = [];
1462: $changed = false;
1463: foreach ($this->types as $innerType) {
1464: $candidates = [];
1465: foreach ($rightTypes as $i => $rightType) {
1466: if (!$innerType->isSuperTypeOf($rightType)->yes()) {
1467: continue;
1468: }
1469:
1470: $candidates[] = $rightType;
1471: unset($rightTypes[$i]);
1472: }
1473:
1474: if (count($candidates) === 0) {
1475: $newTypes[] = $innerType;
1476: continue;
1477: }
1478:
1479: $newType = $cb($innerType, TypeCombinator::union(...$candidates));
1480: if ($innerType !== $newType) {
1481: $changed = true;
1482: }
1483:
1484: $newTypes[] = $newType;
1485: }
1486:
1487: if ($changed) {
1488: return TypeCombinator::union(...$newTypes);
1489: }
1490:
1491: return $this;
1492: }
1493:
1494: public function tryRemove(Type $typeToRemove): ?Type
1495: {
1496: $finiteTypeSet = $this->getFiniteTypeSet();
1497: if ($finiteTypeSet !== null && $finiteTypeSet->isComplete()) {
1498: $key = FiniteTypeSet::key($typeToRemove);
1499: if ($key !== null) {
1500: if (!$finiteTypeSet->has($key)) {
1501: return null;
1502: }
1503:
1504: $remainingTypes = [];
1505: foreach ($finiteTypeSet->getMembers() as $memberKey => $member) {
1506: if ($memberKey === $key) {
1507: continue;
1508: }
1509:
1510: $remainingTypes[] = $member;
1511: }
1512:
1513: if (count($remainingTypes) === 1) {
1514: return $remainingTypes[0];
1515: }
1516:
1517: return new UnionType($remainingTypes);
1518: }
1519: }
1520:
1521: $innerTypes = [];
1522: $changed = false;
1523: foreach ($this->types as $innerType) {
1524: $removed = TypeCombinator::remove($innerType, $typeToRemove);
1525: if (!$removed->equals($innerType)) {
1526: $changed = true;
1527: }
1528: if ($removed instanceof NeverType) {
1529: continue;
1530: }
1531: if ($removed instanceof self && !$removed instanceof TemplateType) {
1532: foreach ($removed->getTypes() as $removedInnerType) {
1533: $innerTypes[] = $removedInnerType;
1534: }
1535: } else {
1536: $innerTypes[] = $removed;
1537: }
1538: }
1539:
1540: if (!$changed) {
1541: return null;
1542: }
1543:
1544: if (count($innerTypes) === 0) {
1545: return new NeverType();
1546: }
1547:
1548: if (count($innerTypes) === 1) {
1549: return $innerTypes[0];
1550: }
1551:
1552: return new UnionType($innerTypes);
1553: }
1554:
1555: public function exponentiate(Type $exponent): Type
1556: {
1557: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1558: }
1559:
1560: public function getFiniteTypes(): array
1561: {
1562: if ($this->finiteTypes !== null) {
1563: return $this->finiteTypes;
1564: }
1565:
1566: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1567: $uniquedTypes = [];
1568: foreach ($types as $type) {
1569: $uniquedTypes[$type->describe(VerbosityLevel::cache())] = $type;
1570: }
1571:
1572: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1573: return $this->finiteTypes = [];
1574: }
1575:
1576: return $this->finiteTypes = array_values($uniquedTypes);
1577: }
1578:
1579: /**
1580: * @param callable(Type $type): TrinaryLogic $getResult
1581: */
1582: protected function unionResults(callable $getResult): TrinaryLogic
1583: {
1584: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1585: }
1586:
1587: /**
1588: * @param callable(Type $type): TrinaryLogic $getResult
1589: */
1590: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1591: {
1592: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1593: }
1594:
1595: /**
1596: * @param callable(Type $type): Type $getType
1597: */
1598: protected function unionTypes(callable $getType): Type
1599: {
1600: $newTypes = [];
1601: $changed = false;
1602: foreach ($this->types as $type) {
1603: $newType = $getType($type);
1604: if ($newType !== $type) {
1605: $changed = true;
1606: }
1607: $newTypes[] = $newType;
1608: }
1609:
1610: if (!$changed) {
1611: return $this;
1612: }
1613:
1614: return TypeCombinator::union(...$newTypes);
1615: }
1616:
1617: /**
1618: * @template T
1619: * @param callable(Type $type): list<T> $getValues
1620: * @param callable(Type $type): bool $criteria
1621: * @return list<T>
1622: */
1623: protected function pickFromTypes(
1624: callable $getValues,
1625: callable $criteria,
1626: ): array
1627: {
1628: $values = [];
1629: foreach ($this->types as $type) {
1630: $innerValues = $getValues($type);
1631: if ($innerValues === []) {
1632: return [];
1633: }
1634:
1635: foreach ($innerValues as $innerType) {
1636: $values[] = $innerType;
1637: }
1638: }
1639:
1640: return $values;
1641: }
1642:
1643: public function toPhpDocNode(): TypeNode
1644: {
1645: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1646: }
1647:
1648: /**
1649: * @template T
1650: * @param callable(Type $type): list<T> $getValues
1651: * @return list<T>
1652: */
1653: private function notBenevolentPickFromTypes(callable $getValues): array
1654: {
1655: $values = [];
1656: foreach ($this->types as $type) {
1657: $innerValues = $getValues($type);
1658: if ($innerValues === []) {
1659: return [];
1660: }
1661:
1662: foreach ($innerValues as $innerType) {
1663: $values[] = $innerType;
1664: }
1665: }
1666:
1667: return $values;
1668: }
1669:
1670: public function hasTemplateOrLateResolvableType(): bool
1671: {
1672: foreach ($this->types as $type) {
1673: if (!$type->hasTemplateOrLateResolvableType()) {
1674: continue;
1675: }
1676:
1677: return true;
1678: }
1679:
1680: return false;
1681: }
1682:
1683: }
1684: