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 isOffsetAccessLegal(): TrinaryLogic
651: {
652: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
653: }
654:
655: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
656: {
657: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
658: }
659:
660: public function getOffsetValueType(Type $offsetType): Type
661: {
662: $types = [];
663: foreach ($this->types as $innerType) {
664: $valueType = $innerType->getOffsetValueType($offsetType);
665: if ($valueType instanceof ErrorType) {
666: continue;
667: }
668:
669: $types[] = $valueType;
670: }
671:
672: if (count($types) === 0) {
673: return new ErrorType();
674: }
675:
676: return TypeCombinator::union(...$types);
677: }
678:
679: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
680: {
681: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
682: }
683:
684: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
685: {
686: return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
687: }
688:
689: public function unsetOffset(Type $offsetType): Type
690: {
691: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
692: }
693:
694: public function getKeysArray(): Type
695: {
696: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray());
697: }
698:
699: public function getValuesArray(): Type
700: {
701: return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
702: }
703:
704: public function fillKeysArray(Type $valueType): Type
705: {
706: return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
707: }
708:
709: public function flipArray(): Type
710: {
711: return $this->unionTypes(static fn (Type $type): Type => $type->flipArray());
712: }
713:
714: public function intersectKeyArray(Type $otherArraysType): Type
715: {
716: return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
717: }
718:
719: public function popArray(): Type
720: {
721: return $this->unionTypes(static fn (Type $type): Type => $type->popArray());
722: }
723:
724: public function searchArray(Type $needleType): Type
725: {
726: return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType));
727: }
728:
729: public function shiftArray(): Type
730: {
731: return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray());
732: }
733:
734: public function shuffleArray(): Type
735: {
736: return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray());
737: }
738:
739: public function getEnumCases(): array
740: {
741: return $this->pickFromTypes(
742: static fn (Type $type) => $type->getEnumCases(),
743: static fn (Type $type) => $type->isObject()->yes(),
744: );
745: }
746:
747: public function isCallable(): TrinaryLogic
748: {
749: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
750: }
751:
752: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
753: {
754: $acceptors = [];
755:
756: foreach ($this->types as $type) {
757: if ($type->isCallable()->no()) {
758: continue;
759: }
760:
761: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
762: }
763:
764: if (count($acceptors) === 0) {
765: throw new ShouldNotHappenException();
766: }
767:
768: return $acceptors;
769: }
770:
771: public function isCloneable(): TrinaryLogic
772: {
773: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
774: }
775:
776: public function isSmallerThan(Type $otherType): TrinaryLogic
777: {
778: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType));
779: }
780:
781: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
782: {
783: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType));
784: }
785:
786: public function isNull(): TrinaryLogic
787: {
788: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
789: }
790:
791: public function isConstantValue(): TrinaryLogic
792: {
793: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
794: }
795:
796: public function isConstantScalarValue(): TrinaryLogic
797: {
798: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
799: }
800:
801: public function getConstantScalarTypes(): array
802: {
803: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
804: }
805:
806: public function getConstantScalarValues(): array
807: {
808: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
809: }
810:
811: public function isTrue(): TrinaryLogic
812: {
813: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
814: }
815:
816: public function isFalse(): TrinaryLogic
817: {
818: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
819: }
820:
821: public function isBoolean(): TrinaryLogic
822: {
823: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
824: }
825:
826: public function isFloat(): TrinaryLogic
827: {
828: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
829: }
830:
831: public function isInteger(): TrinaryLogic
832: {
833: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
834: }
835:
836: public function getSmallerType(): Type
837: {
838: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType());
839: }
840:
841: public function getSmallerOrEqualType(): Type
842: {
843: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType());
844: }
845:
846: public function getGreaterType(): Type
847: {
848: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType());
849: }
850:
851: public function getGreaterOrEqualType(): Type
852: {
853: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType());
854: }
855:
856: public function isGreaterThan(Type $otherType): TrinaryLogic
857: {
858: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type));
859: }
860:
861: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
862: {
863: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type));
864: }
865:
866: public function toBoolean(): BooleanType
867: {
868: /** @var BooleanType $type */
869: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
870:
871: return $type;
872: }
873:
874: public function toNumber(): Type
875: {
876: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
877:
878: return $type;
879: }
880:
881: public function toString(): Type
882: {
883: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
884:
885: return $type;
886: }
887:
888: public function toInteger(): Type
889: {
890: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
891:
892: return $type;
893: }
894:
895: public function toFloat(): Type
896: {
897: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
898:
899: return $type;
900: }
901:
902: public function toArray(): Type
903: {
904: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
905:
906: return $type;
907: }
908:
909: public function toArrayKey(): Type
910: {
911: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
912: }
913:
914: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
915: {
916: $types = TemplateTypeMap::createEmpty();
917: if ($receivedType instanceof UnionType) {
918: $myTypes = [];
919: $remainingReceivedTypes = [];
920: foreach ($receivedType->getTypes() as $receivedInnerType) {
921: foreach ($this->types as $type) {
922: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
923: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
924: continue 2;
925: }
926: $myTypes[] = $type;
927: }
928: $remainingReceivedTypes[] = $receivedInnerType;
929: }
930: if (count($remainingReceivedTypes) === 0) {
931: return $types;
932: }
933: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
934: } else {
935: $myTypes = $this->types;
936: }
937:
938: foreach ($myTypes as $type) {
939: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
940: continue;
941: }
942: $types = $types->union($type->inferTemplateTypes($receivedType));
943: }
944:
945: if (!$types->isEmpty()) {
946: return $types;
947: }
948:
949: foreach ($myTypes as $type) {
950: $types = $types->union($type->inferTemplateTypes($receivedType));
951: }
952:
953: return $types;
954: }
955:
956: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
957: {
958: $types = TemplateTypeMap::createEmpty();
959:
960: foreach ($this->types as $type) {
961: $types = $types->union($templateType->inferTemplateTypes($type));
962: }
963:
964: return $types;
965: }
966:
967: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
968: {
969: $references = [];
970:
971: foreach ($this->types as $type) {
972: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
973: $references[] = $reference;
974: }
975: }
976:
977: return $references;
978: }
979:
980: public function traverse(callable $cb): Type
981: {
982: $types = [];
983: $changed = false;
984:
985: foreach ($this->types as $type) {
986: $newType = $cb($type);
987: if ($type !== $newType) {
988: $changed = true;
989: }
990: $types[] = $newType;
991: }
992:
993: if ($changed) {
994: return TypeCombinator::union(...$types);
995: }
996:
997: return $this;
998: }
999:
1000: public function traverseSimultaneously(Type $right, callable $cb): Type
1001: {
1002: $types = [];
1003: $changed = false;
1004:
1005: if (!$right instanceof self) {
1006: return $this;
1007: }
1008:
1009: if (count($this->getTypes()) !== count($right->getTypes())) {
1010: return $this;
1011: }
1012:
1013: foreach ($this->getSortedTypes() as $i => $leftType) {
1014: $rightType = $right->getSortedTypes()[$i];
1015: $newType = $cb($leftType, $rightType);
1016: if ($leftType !== $newType) {
1017: $changed = true;
1018: }
1019: $types[] = $newType;
1020: }
1021:
1022: if ($changed) {
1023: return TypeCombinator::union(...$types);
1024: }
1025:
1026: return $this;
1027: }
1028:
1029: public function tryRemove(Type $typeToRemove): ?Type
1030: {
1031: return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1032: }
1033:
1034: public function exponentiate(Type $exponent): Type
1035: {
1036: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1037: }
1038:
1039: public function getFiniteTypes(): array
1040: {
1041: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1042: $uniquedTypes = [];
1043: foreach ($types as $type) {
1044: $uniquedTypes[md5($type->describe(VerbosityLevel::cache()))] = $type;
1045: }
1046:
1047: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1048: return [];
1049: }
1050:
1051: return array_values($uniquedTypes);
1052: }
1053:
1054: /**
1055: * @param mixed[] $properties
1056: */
1057: public static function __set_state(array $properties): Type
1058: {
1059: return new self($properties['types'], $properties['normalized']);
1060: }
1061:
1062: /**
1063: * @param callable(Type $type): TrinaryLogic $getResult
1064: */
1065: protected function unionResults(callable $getResult): TrinaryLogic
1066: {
1067: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1068: }
1069:
1070: /**
1071: * @param callable(Type $type): TrinaryLogic $getResult
1072: */
1073: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1074: {
1075: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1076: }
1077:
1078: /**
1079: * @param callable(Type $type): Type $getType
1080: */
1081: protected function unionTypes(callable $getType): Type
1082: {
1083: return TypeCombinator::union(...array_map($getType, $this->types));
1084: }
1085:
1086: /**
1087: * @template T of Type
1088: * @param callable(Type $type): list<T> $getTypes
1089: * @return list<T>
1090: *
1091: * @deprecated Use pickFromTypes() instead.
1092: */
1093: protected function pickTypes(callable $getTypes): array
1094: {
1095: return $this->pickFromTypes($getTypes, static fn () => false);
1096: }
1097:
1098: /**
1099: * @template T
1100: * @param callable(Type $type): list<T> $getValues
1101: * @param callable(Type $type): bool $criteria
1102: * @return list<T>
1103: */
1104: protected function pickFromTypes(
1105: callable $getValues,
1106: callable $criteria,
1107: ): array
1108: {
1109: $values = [];
1110: foreach ($this->types as $type) {
1111: $innerValues = $getValues($type);
1112: if ($innerValues === []) {
1113: return [];
1114: }
1115:
1116: foreach ($innerValues as $innerType) {
1117: $values[] = $innerType;
1118: }
1119: }
1120:
1121: return $values;
1122: }
1123:
1124: public function toPhpDocNode(): TypeNode
1125: {
1126: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1127: }
1128:
1129: /**
1130: * @template T
1131: * @param callable(Type $type): list<T> $getValues
1132: * @return list<T>
1133: */
1134: private function notBenevolentPickFromTypes(callable $getValues): array
1135: {
1136: $values = [];
1137: foreach ($this->types as $type) {
1138: $innerValues = $getValues($type);
1139: if ($innerValues === []) {
1140: return [];
1141: }
1142:
1143: foreach ($innerValues as $innerType) {
1144: $values[] = $innerType;
1145: }
1146: }
1147:
1148: return $values;
1149: }
1150:
1151: }
1152: