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