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