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