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