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