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