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