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