1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use DateTime;
6: use DateTimeImmutable;
7: use DateTimeInterface;
8: use PHPStan\Php\PhpVersion;
9: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
10: use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
11: use PHPStan\Reflection\ClassConstantReflection;
12: use PHPStan\Reflection\ClassMemberAccessAnswerer;
13: use PHPStan\Reflection\ExtendedMethodReflection;
14: use PHPStan\Reflection\ExtendedPropertyReflection;
15: use PHPStan\Reflection\InitializerExprTypeResolver;
16: use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection;
17: use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection;
18: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
19: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
20: use PHPStan\ShouldNotHappenException;
21: use PHPStan\TrinaryLogic;
22: use PHPStan\Type\Generic\GenericClassStringType;
23: use PHPStan\Type\Generic\TemplateMixedType;
24: use PHPStan\Type\Generic\TemplateType;
25: use PHPStan\Type\Generic\TemplateTypeMap;
26: use PHPStan\Type\Generic\TemplateTypeVariance;
27: use PHPStan\Type\Generic\TemplateUnionType;
28: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
29: use function array_diff_assoc;
30: use function array_fill_keys;
31: use function array_map;
32: use function array_merge;
33: use function array_slice;
34: use function array_unique;
35: use function array_values;
36: use function count;
37: use function implode;
38: use function md5;
39: use function sprintf;
40: use function str_contains;
41:
42: /** @api */
43: class UnionType implements CompoundType
44: {
45:
46: use NonGeneralizableTypeTrait;
47:
48: private bool $sortedTypes = false;
49:
50: /** @var array<int, string> */
51: private array $cachedDescriptions = [];
52:
53: /**
54: * @api
55: * @param Type[] $types
56: */
57: public function __construct(private array $types, private bool $normalized = false)
58: {
59: $throwException = static function () use ($types): void {
60: throw new ShouldNotHappenException(sprintf(
61: 'Cannot create %s with: %s',
62: self::class,
63: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
64: ));
65: };
66: if (count($types) < 2) {
67: $throwException();
68: }
69: foreach ($types as $type) {
70: if (!($type instanceof UnionType)) {
71: continue;
72: }
73: if ($type instanceof TemplateType) {
74: continue;
75: }
76:
77: $throwException();
78: }
79: }
80:
81: /**
82: * @return Type[]
83: */
84: public function getTypes(): array
85: {
86: return $this->types;
87: }
88:
89: public function isNormalized(): bool
90: {
91: return $this->normalized;
92: }
93:
94: /**
95: * @return Type[]
96: */
97: protected function getSortedTypes(): array
98: {
99: if ($this->sortedTypes) {
100: return $this->types;
101: }
102:
103: $this->types = UnionTypeHelper::sortTypes($this->types);
104: $this->sortedTypes = true;
105:
106: return $this->types;
107: }
108:
109: public function getReferencedClasses(): array
110: {
111: $classes = [];
112: foreach ($this->types as $type) {
113: foreach ($type->getReferencedClasses() as $className) {
114: $classes[] = $className;
115: }
116: }
117:
118: return $classes;
119: }
120:
121: public function getObjectClassNames(): array
122: {
123: return array_values(array_unique($this->pickFromTypes(
124: static fn (Type $type) => $type->getObjectClassNames(),
125: static fn (Type $type) => $type->isObject()->yes(),
126: )));
127: }
128:
129: public function getObjectClassReflections(): array
130: {
131: return $this->pickFromTypes(
132: static fn (Type $type) => $type->getObjectClassReflections(),
133: static fn (Type $type) => $type->isObject()->yes(),
134: );
135: }
136:
137: public function getArrays(): array
138: {
139: return $this->pickFromTypes(
140: static fn (Type $type) => $type->getArrays(),
141: static fn (Type $type) => $type->isArray()->yes(),
142: );
143: }
144:
145: public function getConstantArrays(): array
146: {
147: return $this->pickFromTypes(
148: static fn (Type $type) => $type->getConstantArrays(),
149: static fn (Type $type) => $type->isArray()->yes(),
150: );
151: }
152:
153: public function getConstantStrings(): array
154: {
155: return $this->pickFromTypes(
156: static fn (Type $type) => $type->getConstantStrings(),
157: static fn (Type $type) => $type->isString()->yes(),
158: );
159: }
160:
161: public function accepts(Type $type, bool $strictTypes): AcceptsResult
162: {
163: if (
164: $type->equals(new ObjectType(DateTimeInterface::class))
165: && $this->accepts(
166: new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]),
167: $strictTypes,
168: )->yes()
169: ) {
170: return AcceptsResult::createYes();
171: }
172:
173: $result = AcceptsResult::createNo();
174: foreach ($this->getSortedTypes() as $i => $innerType) {
175: $result = $result->or($innerType->accepts($type, $strictTypes)->decorateReasons(static fn (string $reason) => sprintf('Type #%d from the union: %s', $i + 1, $reason)));
176: }
177: if ($result->yes()) {
178: return $result;
179: }
180:
181: if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) {
182: return $type->isAcceptedBy($this, $strictTypes);
183: }
184:
185: if ($type instanceof TemplateUnionType) {
186: return $result->or($type->isAcceptedBy($this, $strictTypes));
187: }
188:
189: if ($type->isEnum()->yes() && !$this->isEnum()->no()) {
190: $enumCasesUnion = TypeCombinator::union(...$type->getEnumCases());
191: if (!$type->equals($enumCasesUnion)) {
192: return $this->accepts($enumCasesUnion, $strictTypes);
193: }
194: }
195:
196: return $result;
197: }
198:
199: public function isSuperTypeOf(Type $otherType): IsSuperTypeOfResult
200: {
201: if (
202: ($otherType instanceof self && !$otherType instanceof TemplateUnionType)
203: || $otherType instanceof IterableType
204: || $otherType instanceof NeverType
205: || $otherType instanceof ConditionalType
206: || $otherType instanceof ConditionalTypeForParameter
207: || $otherType instanceof IntegerRangeType
208: ) {
209: return $otherType->isSubTypeOf($this);
210: }
211:
212: $result = IsSuperTypeOfResult::createNo()->or(...array_map(static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType), $this->types));
213: if ($result->yes()) {
214: return $result;
215: }
216:
217: if ($otherType instanceof TemplateUnionType) {
218: return $result->or($otherType->isSubTypeOf($this));
219: }
220:
221: return $result;
222: }
223:
224: public function isSubTypeOf(Type $otherType): IsSuperTypeOfResult
225: {
226: return IsSuperTypeOfResult::extremeIdentity(...array_map(static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType), $this->types));
227: }
228:
229: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): AcceptsResult
230: {
231: return AcceptsResult::extremeIdentity(...array_map(static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes), $this->types));
232: }
233:
234: public function equals(Type $type): bool
235: {
236: if (!$type instanceof static) {
237: return false;
238: }
239:
240: if (count($this->types) !== count($type->types)) {
241: return false;
242: }
243:
244: $otherTypes = $type->types;
245: foreach ($this->types as $innerType) {
246: $match = false;
247: foreach ($otherTypes as $i => $otherType) {
248: if (!$innerType->equals($otherType)) {
249: continue;
250: }
251:
252: $match = true;
253: unset($otherTypes[$i]);
254: break;
255: }
256:
257: if (!$match) {
258: return false;
259: }
260: }
261:
262: return count($otherTypes) === 0;
263: }
264:
265: public function describe(VerbosityLevel $level): string
266: {
267: if (isset($this->cachedDescriptions[$level->getLevelValue()])) {
268: return $this->cachedDescriptions[$level->getLevelValue()];
269: }
270: $joinTypes = static function (array $types) use ($level): string {
271: $typeNames = [];
272: foreach ($types as $i => $type) {
273: if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) {
274: $typeNames[] = sprintf('(%s)', $type->describe($level));
275: } elseif ($type instanceof TemplateType) {
276: $isLast = $i >= count($types) - 1;
277: $bound = $type->getBound();
278: if (
279: !$isLast
280: && ($level->isTypeOnly() || $level->isValue())
281: && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType)
282: ) {
283: $typeNames[] = sprintf('(%s)', $type->describe($level));
284: } else {
285: $typeNames[] = $type->describe($level);
286: }
287: } elseif ($type instanceof IntersectionType) {
288: $intersectionDescription = $type->describe($level);
289: if (str_contains($intersectionDescription, '&')) {
290: $typeNames[] = sprintf('(%s)', $type->describe($level));
291: } else {
292: $typeNames[] = $intersectionDescription;
293: }
294: } else {
295: $typeNames[] = $type->describe($level);
296: }
297: }
298:
299: if ($level->isPrecise()) {
300: $duplicates = array_diff_assoc($typeNames, array_unique($typeNames));
301: if (count($duplicates) > 0) {
302: $indexByDuplicate = array_fill_keys($duplicates, 0);
303: foreach ($typeNames as $key => $typeName) {
304: if (!isset($indexByDuplicate[$typeName])) {
305: continue;
306: }
307:
308: $typeNames[$key] = $typeName . '#' . ++$indexByDuplicate[$typeName];
309: }
310: }
311: } else {
312: $typeNames = array_unique($typeNames);
313: }
314:
315: if (count($typeNames) > 1024) {
316: return implode('|', array_slice($typeNames, 0, 1024)) . "|\u{2026}";
317: }
318:
319: return implode('|', $typeNames);
320: };
321:
322: return $this->cachedDescriptions[$level->getLevelValue()] = $level->handle(
323: function () use ($joinTypes): string {
324: $types = TypeCombinator::union(...array_map(static function (Type $type): Type {
325: if (
326: $type->isConstantValue()->yes()
327: && $type->isTrue()->or($type->isFalse())->no()
328: ) {
329: return $type->generalize(GeneralizePrecision::lessSpecific());
330: }
331:
332: return $type;
333: }, $this->getSortedTypes()));
334:
335: if ($types instanceof UnionType) {
336: return $joinTypes($types->getSortedTypes());
337: }
338:
339: return $joinTypes([$types]);
340: },
341: fn (): string => $joinTypes($this->getSortedTypes()),
342: );
343: }
344:
345: /**
346: * @param callable(Type $type): TrinaryLogic $canCallback
347: * @param callable(Type $type): TrinaryLogic $hasCallback
348: */
349: private function hasInternal(
350: callable $canCallback,
351: callable $hasCallback,
352: ): TrinaryLogic
353: {
354: return TrinaryLogic::lazyExtremeIdentity($this->types, static function (Type $type) use ($canCallback, $hasCallback): TrinaryLogic {
355: if ($canCallback($type)->no()) {
356: return TrinaryLogic::createNo();
357: }
358:
359: return $hasCallback($type);
360: });
361: }
362:
363: /**
364: * @template TObject of object
365: * @param callable(Type $type): TrinaryLogic $hasCallback
366: * @param callable(Type $type): TObject $getCallback
367: * @return TObject
368: */
369: private function getInternal(
370: callable $hasCallback,
371: callable $getCallback,
372: ): object
373: {
374: /** @var TrinaryLogic|null $result */
375: $result = null;
376:
377: /** @var TObject|null $object */
378: $object = null;
379: foreach ($this->types as $type) {
380: $has = $hasCallback($type);
381: if (!$has->yes()) {
382: continue;
383: }
384: if ($result !== null && $result->compareTo($has) !== $has) {
385: continue;
386: }
387:
388: $get = $getCallback($type);
389: $result = $has;
390: $object = $get;
391: }
392:
393: if ($object === null) {
394: throw new ShouldNotHappenException();
395: }
396:
397: return $object;
398: }
399:
400: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
401: {
402: return $this->unionTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
403: }
404:
405: public function isObject(): TrinaryLogic
406: {
407: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isObject());
408: }
409:
410: public function isEnum(): TrinaryLogic
411: {
412: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
413: }
414:
415: public function canAccessProperties(): TrinaryLogic
416: {
417: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
418: }
419:
420: public function hasProperty(string $propertyName): TrinaryLogic
421: {
422: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
423: }
424:
425: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
426: {
427: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
428: }
429:
430: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
431: {
432: $propertyPrototypes = [];
433: foreach ($this->types as $type) {
434: if (!$type->hasProperty($propertyName)->yes()) {
435: continue;
436: }
437:
438: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
439: }
440:
441: $propertiesCount = count($propertyPrototypes);
442: if ($propertiesCount === 0) {
443: throw new ShouldNotHappenException();
444: }
445:
446: if ($propertiesCount === 1) {
447: return $propertyPrototypes[0];
448: }
449:
450: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
451: }
452:
453: public function canCallMethods(): TrinaryLogic
454: {
455: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
456: }
457:
458: public function hasMethod(string $methodName): TrinaryLogic
459: {
460: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
461: }
462:
463: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
464: {
465: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
466: }
467:
468: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
469: {
470: $methodPrototypes = [];
471: foreach ($this->types as $type) {
472: if (!$type->hasMethod($methodName)->yes()) {
473: continue;
474: }
475:
476: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
477: }
478:
479: $methodsCount = count($methodPrototypes);
480: if ($methodsCount === 0) {
481: throw new ShouldNotHappenException();
482: }
483:
484: if ($methodsCount === 1) {
485: return $methodPrototypes[0];
486: }
487:
488: return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
489: }
490:
491: public function canAccessConstants(): TrinaryLogic
492: {
493: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
494: }
495:
496: public function hasConstant(string $constantName): TrinaryLogic
497: {
498: return $this->hasInternal(
499: static fn (Type $type): TrinaryLogic => $type->canAccessConstants(),
500: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
501: );
502: }
503:
504: public function getConstant(string $constantName): ClassConstantReflection
505: {
506: return $this->getInternal(
507: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
508: static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName),
509: );
510: }
511:
512: public function isIterable(): TrinaryLogic
513: {
514: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
515: }
516:
517: public function isIterableAtLeastOnce(): TrinaryLogic
518: {
519: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
520: }
521:
522: public function getArraySize(): Type
523: {
524: return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize());
525: }
526:
527: public function getIterableKeyType(): Type
528: {
529: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
530: }
531:
532: public function getFirstIterableKeyType(): Type
533: {
534: return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType());
535: }
536:
537: public function getLastIterableKeyType(): Type
538: {
539: return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableKeyType());
540: }
541:
542: public function getIterableValueType(): Type
543: {
544: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
545: }
546:
547: public function getFirstIterableValueType(): Type
548: {
549: return $this->unionTypes(static fn (Type $type): Type => $type->getFirstIterableValueType());
550: }
551:
552: public function getLastIterableValueType(): Type
553: {
554: return $this->unionTypes(static fn (Type $type): Type => $type->getLastIterableValueType());
555: }
556:
557: public function isArray(): TrinaryLogic
558: {
559: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray());
560: }
561:
562: public function isConstantArray(): TrinaryLogic
563: {
564: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
565: }
566:
567: public function isOversizedArray(): TrinaryLogic
568: {
569: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
570: }
571:
572: public function isList(): TrinaryLogic
573: {
574: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList());
575: }
576:
577: public function isString(): TrinaryLogic
578: {
579: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString());
580: }
581:
582: public function isNumericString(): TrinaryLogic
583: {
584: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
585: }
586:
587: public function isNonEmptyString(): TrinaryLogic
588: {
589: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
590: }
591:
592: public function isNonFalsyString(): TrinaryLogic
593: {
594: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
595: }
596:
597: public function isLiteralString(): TrinaryLogic
598: {
599: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
600: }
601:
602: public function isLowercaseString(): TrinaryLogic
603: {
604: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
605: }
606:
607: public function isClassString(): TrinaryLogic
608: {
609: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
610: }
611:
612: public function getClassStringObjectType(): Type
613: {
614: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
615: }
616:
617: public function getObjectTypeOrClassStringObjectType(): Type
618: {
619: return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
620: }
621:
622: public function isVoid(): TrinaryLogic
623: {
624: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
625: }
626:
627: public function isScalar(): TrinaryLogic
628: {
629: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
630: }
631:
632: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
633: {
634: return new BooleanType();
635: }
636:
637: public function isOffsetAccessible(): TrinaryLogic
638: {
639: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
640: }
641:
642: public function isOffsetAccessLegal(): TrinaryLogic
643: {
644: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
645: }
646:
647: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
648: {
649: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
650: }
651:
652: public function getOffsetValueType(Type $offsetType): Type
653: {
654: $types = [];
655: foreach ($this->types as $innerType) {
656: $valueType = $innerType->getOffsetValueType($offsetType);
657: if ($valueType instanceof ErrorType) {
658: continue;
659: }
660:
661: $types[] = $valueType;
662: }
663:
664: if (count($types) === 0) {
665: return new ErrorType();
666: }
667:
668: return TypeCombinator::union(...$types);
669: }
670:
671: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
672: {
673: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
674: }
675:
676: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
677: {
678: return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
679: }
680:
681: public function unsetOffset(Type $offsetType): Type
682: {
683: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
684: }
685:
686: public function getKeysArray(): Type
687: {
688: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray());
689: }
690:
691: public function getValuesArray(): Type
692: {
693: return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
694: }
695:
696: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
697: {
698: return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
699: }
700:
701: public function fillKeysArray(Type $valueType): Type
702: {
703: return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
704: }
705:
706: public function flipArray(): Type
707: {
708: return $this->unionTypes(static fn (Type $type): Type => $type->flipArray());
709: }
710:
711: public function intersectKeyArray(Type $otherArraysType): Type
712: {
713: return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
714: }
715:
716: public function popArray(): Type
717: {
718: return $this->unionTypes(static fn (Type $type): Type => $type->popArray());
719: }
720:
721: public function reverseArray(TrinaryLogic $preserveKeys): Type
722: {
723: return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
724: }
725:
726: public function searchArray(Type $needleType): Type
727: {
728: return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType));
729: }
730:
731: public function shiftArray(): Type
732: {
733: return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray());
734: }
735:
736: public function shuffleArray(): Type
737: {
738: return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray());
739: }
740:
741: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
742: {
743: return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
744: }
745:
746: public function getEnumCases(): array
747: {
748: return $this->pickFromTypes(
749: static fn (Type $type) => $type->getEnumCases(),
750: static fn (Type $type) => $type->isObject()->yes(),
751: );
752: }
753:
754: public function isCallable(): TrinaryLogic
755: {
756: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
757: }
758:
759: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
760: {
761: $acceptors = [];
762:
763: foreach ($this->types as $type) {
764: if ($type->isCallable()->no()) {
765: continue;
766: }
767:
768: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
769: }
770:
771: if (count($acceptors) === 0) {
772: throw new ShouldNotHappenException();
773: }
774:
775: return $acceptors;
776: }
777:
778: public function isCloneable(): TrinaryLogic
779: {
780: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
781: }
782:
783: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
784: {
785: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
786: }
787:
788: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
789: {
790: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
791: }
792:
793: public function isNull(): TrinaryLogic
794: {
795: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
796: }
797:
798: public function isConstantValue(): TrinaryLogic
799: {
800: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
801: }
802:
803: public function isConstantScalarValue(): TrinaryLogic
804: {
805: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
806: }
807:
808: public function getConstantScalarTypes(): array
809: {
810: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
811: }
812:
813: public function getConstantScalarValues(): array
814: {
815: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
816: }
817:
818: public function isTrue(): TrinaryLogic
819: {
820: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
821: }
822:
823: public function isFalse(): TrinaryLogic
824: {
825: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
826: }
827:
828: public function isBoolean(): TrinaryLogic
829: {
830: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
831: }
832:
833: public function isFloat(): TrinaryLogic
834: {
835: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
836: }
837:
838: public function isInteger(): TrinaryLogic
839: {
840: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
841: }
842:
843: public function getSmallerType(PhpVersion $phpVersion): Type
844: {
845: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
846: }
847:
848: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
849: {
850: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
851: }
852:
853: public function getGreaterType(PhpVersion $phpVersion): Type
854: {
855: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
856: }
857:
858: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
859: {
860: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
861: }
862:
863: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
864: {
865: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
866: }
867:
868: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
869: {
870: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
871: }
872:
873: public function toBoolean(): BooleanType
874: {
875: /** @var BooleanType $type */
876: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
877:
878: return $type;
879: }
880:
881: public function toNumber(): Type
882: {
883: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
884:
885: return $type;
886: }
887:
888: public function toAbsoluteNumber(): Type
889: {
890: $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
891:
892: return $type;
893: }
894:
895: public function toString(): Type
896: {
897: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
898:
899: return $type;
900: }
901:
902: public function toInteger(): Type
903: {
904: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
905:
906: return $type;
907: }
908:
909: public function toFloat(): Type
910: {
911: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
912:
913: return $type;
914: }
915:
916: public function toArray(): Type
917: {
918: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
919:
920: return $type;
921: }
922:
923: public function toArrayKey(): Type
924: {
925: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
926: }
927:
928: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
929: {
930: $types = TemplateTypeMap::createEmpty();
931: if ($receivedType instanceof UnionType) {
932: $myTypes = [];
933: $remainingReceivedTypes = [];
934: foreach ($receivedType->getTypes() as $receivedInnerType) {
935: foreach ($this->types as $type) {
936: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
937: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
938: continue 2;
939: }
940: $myTypes[] = $type;
941: }
942: $remainingReceivedTypes[] = $receivedInnerType;
943: }
944: if (count($remainingReceivedTypes) === 0) {
945: return $types;
946: }
947: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
948: } else {
949: $myTypes = $this->types;
950: }
951:
952: foreach ($myTypes as $type) {
953: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
954: continue;
955: }
956: $types = $types->union($type->inferTemplateTypes($receivedType));
957: }
958:
959: if (!$types->isEmpty()) {
960: return $types;
961: }
962:
963: foreach ($myTypes as $type) {
964: $types = $types->union($type->inferTemplateTypes($receivedType));
965: }
966:
967: return $types;
968: }
969:
970: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
971: {
972: $types = TemplateTypeMap::createEmpty();
973:
974: foreach ($this->types as $type) {
975: $types = $types->union($templateType->inferTemplateTypes($type));
976: }
977:
978: return $types;
979: }
980:
981: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
982: {
983: $references = [];
984:
985: foreach ($this->types as $type) {
986: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
987: $references[] = $reference;
988: }
989: }
990:
991: return $references;
992: }
993:
994: public function traverse(callable $cb): Type
995: {
996: $types = [];
997: $changed = false;
998:
999: foreach ($this->types as $type) {
1000: $newType = $cb($type);
1001: if ($type !== $newType) {
1002: $changed = true;
1003: }
1004: $types[] = $newType;
1005: }
1006:
1007: if ($changed) {
1008: return TypeCombinator::union(...$types);
1009: }
1010:
1011: return $this;
1012: }
1013:
1014: public function traverseSimultaneously(Type $right, callable $cb): Type
1015: {
1016: $types = [];
1017: $changed = false;
1018:
1019: if (!$right instanceof self) {
1020: return $this;
1021: }
1022:
1023: if (count($this->getTypes()) !== count($right->getTypes())) {
1024: return $this;
1025: }
1026:
1027: foreach ($this->getSortedTypes() as $i => $leftType) {
1028: $rightType = $right->getSortedTypes()[$i];
1029: $newType = $cb($leftType, $rightType);
1030: if ($leftType !== $newType) {
1031: $changed = true;
1032: }
1033: $types[] = $newType;
1034: }
1035:
1036: if ($changed) {
1037: return TypeCombinator::union(...$types);
1038: }
1039:
1040: return $this;
1041: }
1042:
1043: public function tryRemove(Type $typeToRemove): ?Type
1044: {
1045: return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1046: }
1047:
1048: public function exponentiate(Type $exponent): Type
1049: {
1050: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1051: }
1052:
1053: public function getFiniteTypes(): array
1054: {
1055: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1056: $uniquedTypes = [];
1057: foreach ($types as $type) {
1058: $uniquedTypes[md5($type->describe(VerbosityLevel::cache()))] = $type;
1059: }
1060:
1061: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1062: return [];
1063: }
1064:
1065: return array_values($uniquedTypes);
1066: }
1067:
1068: /**
1069: * @param callable(Type $type): TrinaryLogic $getResult
1070: */
1071: protected function unionResults(callable $getResult): TrinaryLogic
1072: {
1073: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1074: }
1075:
1076: /**
1077: * @param callable(Type $type): TrinaryLogic $getResult
1078: */
1079: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1080: {
1081: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1082: }
1083:
1084: /**
1085: * @param callable(Type $type): Type $getType
1086: */
1087: protected function unionTypes(callable $getType): Type
1088: {
1089: return TypeCombinator::union(...array_map($getType, $this->types));
1090: }
1091:
1092: /**
1093: * @template T
1094: * @param callable(Type $type): list<T> $getValues
1095: * @param callable(Type $type): bool $criteria
1096: * @return list<T>
1097: */
1098: protected function pickFromTypes(
1099: callable $getValues,
1100: callable $criteria,
1101: ): array
1102: {
1103: $values = [];
1104: foreach ($this->types as $type) {
1105: $innerValues = $getValues($type);
1106: if ($innerValues === []) {
1107: return [];
1108: }
1109:
1110: foreach ($innerValues as $innerType) {
1111: $values[] = $innerType;
1112: }
1113: }
1114:
1115: return $values;
1116: }
1117:
1118: public function toPhpDocNode(): TypeNode
1119: {
1120: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1121: }
1122:
1123: /**
1124: * @template T
1125: * @param callable(Type $type): list<T> $getValues
1126: * @return list<T>
1127: */
1128: private function notBenevolentPickFromTypes(callable $getValues): array
1129: {
1130: $values = [];
1131: foreach ($this->types as $type) {
1132: $innerValues = $getValues($type);
1133: if ($innerValues === []) {
1134: return [];
1135: }
1136:
1137: foreach ($innerValues as $innerType) {
1138: $values[] = $innerType;
1139: }
1140: }
1141:
1142: return $values;
1143: }
1144:
1145: }
1146: