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: $prototype = $type->getUnresolvedMethodPrototype($methodName, $scope);
594: if ($this instanceof TemplateType) {
595: $prototype = $prototype->withCalledOnType($this);
596: }
597: $methodPrototypes[] = $prototype;
598: }
599:
600: $methodsCount = count($methodPrototypes);
601: if ($methodsCount === 0) {
602: throw new MissingMethodFromReflectionException($this->describe(VerbosityLevel::typeOnly()), $methodName);
603: }
604:
605: if ($methodsCount === 1) {
606: return $methodPrototypes[0];
607: }
608:
609: return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
610: }
611:
612: public function canAccessConstants(): TrinaryLogic
613: {
614: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
615: }
616:
617: public function hasConstant(string $constantName): TrinaryLogic
618: {
619: return $this->hasInternal(
620: static fn (Type $type): TrinaryLogic => $type->canAccessConstants(),
621: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
622: );
623: }
624:
625: public function getConstant(string $constantName): ClassConstantReflection
626: {
627: return $this->getInternal(
628: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
629: static fn (Type $type): ClassConstantReflection => $type->getConstant($constantName),
630: );
631: }
632:
633: public function isIterable(): TrinaryLogic
634: {
635: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
636: }
637:
638: public function isIterableAtLeastOnce(): TrinaryLogic
639: {
640: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
641: }
642:
643: public function getArraySize(): Type
644: {
645: return $this->unionTypes(static fn (Type $type): Type => $type->getArraySize());
646: }
647:
648: public function getIterableKeyType(): Type
649: {
650: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
651: }
652:
653: public function getFirstIterableKeyType(): Type
654: {
655: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
656: }
657:
658: public function getLastIterableKeyType(): Type
659: {
660: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
661: }
662:
663: public function getIterableValueType(): Type
664: {
665: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
666: }
667:
668: public function getFirstIterableValueType(): Type
669: {
670: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
671: }
672:
673: public function getLastIterableValueType(): Type
674: {
675: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
676: }
677:
678: public function isArray(): TrinaryLogic
679: {
680: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray());
681: }
682:
683: public function isConstantArray(): TrinaryLogic
684: {
685: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
686: }
687:
688: public function isOversizedArray(): TrinaryLogic
689: {
690: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
691: }
692:
693: public function isList(): TrinaryLogic
694: {
695: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isList());
696: }
697:
698: public function isString(): TrinaryLogic
699: {
700: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString());
701: }
702:
703: public function isNumericString(): TrinaryLogic
704: {
705: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
706: }
707:
708: public function isDecimalIntegerString(): TrinaryLogic
709: {
710: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isDecimalIntegerString());
711: }
712:
713: public function isNonEmptyString(): TrinaryLogic
714: {
715: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
716: }
717:
718: public function isNonFalsyString(): TrinaryLogic
719: {
720: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
721: }
722:
723: public function isLiteralString(): TrinaryLogic
724: {
725: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
726: }
727:
728: public function isLowercaseString(): TrinaryLogic
729: {
730: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLowercaseString());
731: }
732:
733: public function isUppercaseString(): TrinaryLogic
734: {
735: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isUppercaseString());
736: }
737:
738: public function isClassString(): TrinaryLogic
739: {
740: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isClassString());
741: }
742:
743: public function getClassStringObjectType(): Type
744: {
745: return $this->unionTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
746: }
747:
748: public function getObjectTypeOrClassStringObjectType(): Type
749: {
750: return $this->unionTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
751: }
752:
753: public function isVoid(): TrinaryLogic
754: {
755: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
756: }
757:
758: public function isScalar(): TrinaryLogic
759: {
760: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
761: }
762:
763: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
764: {
765: return $this->notBenevolentUnionResults(
766: static fn (Type $innerType): TrinaryLogic => $innerType->looseCompare($type, $phpVersion)->toTrinaryLogic(),
767: )->toBooleanType();
768: }
769:
770: public function isOffsetAccessible(): TrinaryLogic
771: {
772: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
773: }
774:
775: public function isOffsetAccessLegal(): TrinaryLogic
776: {
777: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
778: }
779:
780: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
781: {
782: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
783: }
784:
785: public function getOffsetValueType(Type $offsetType): Type
786: {
787: $types = [];
788: foreach ($this->types as $innerType) {
789: $valueType = $innerType->getOffsetValueType($offsetType);
790: if ($valueType instanceof ErrorType) {
791: continue;
792: }
793:
794: $types[] = $valueType;
795: }
796:
797: if (count($types) === 0) {
798: return new ErrorType();
799: }
800:
801: return TypeCombinator::union(...$types);
802: }
803:
804: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
805: {
806: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
807: }
808:
809: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
810: {
811: return $this->unionTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
812: }
813:
814: public function unsetOffset(Type $offsetType): Type
815: {
816: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
817: }
818:
819: public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
820: {
821: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArrayFiltered($filterValueType, $strict));
822: }
823:
824: public function getKeysArray(): Type
825: {
826: return $this->unionTypes(static fn (Type $type): Type => $type->getKeysArray());
827: }
828:
829: public function getValuesArray(): Type
830: {
831: return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
832: }
833:
834: public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
835: {
836: return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
837: }
838:
839: public function fillKeysArray(Type $valueType): Type
840: {
841: return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
842: }
843:
844: public function flipArray(): Type
845: {
846: return $this->unionTypes(static fn (Type $type): Type => $type->flipArray());
847: }
848:
849: public function intersectKeyArray(Type $otherArraysType): Type
850: {
851: return $this->unionTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
852: }
853:
854: public function popArray(): Type
855: {
856: return $this->unionTypes(static fn (Type $type): Type => $type->popArray());
857: }
858:
859: public function reverseArray(TrinaryLogic $preserveKeys): Type
860: {
861: return $this->unionTypes(static fn (Type $type): Type => $type->reverseArray($preserveKeys));
862: }
863:
864: public function searchArray(Type $needleType, ?TrinaryLogic $strict = null): Type
865: {
866: return $this->unionTypes(static fn (Type $type): Type => $type->searchArray($needleType, $strict));
867: }
868:
869: public function shiftArray(): Type
870: {
871: return $this->unionTypes(static fn (Type $type): Type => $type->shiftArray());
872: }
873:
874: public function shuffleArray(): Type
875: {
876: return $this->unionTypes(static fn (Type $type): Type => $type->shuffleArray());
877: }
878:
879: public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
880: {
881: return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
882: }
883:
884: public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
885: {
886: return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
887: }
888:
889: public function getEnumCases(): array
890: {
891: return $this->pickFromTypes(
892: static fn (Type $type) => $type->getEnumCases(),
893: static fn (Type $type) => $type->isObject()->yes(),
894: );
895: }
896:
897: public function getEnumCaseObject(): ?EnumCaseObjectType
898: {
899: $cases = $this->getEnumCases();
900:
901: if (count($cases) === 1) {
902: return $cases[0];
903: }
904:
905: return null;
906: }
907:
908: public function isCallable(): TrinaryLogic
909: {
910: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
911: }
912:
913: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
914: {
915: $acceptors = [];
916:
917: foreach ($this->types as $type) {
918: if ($type->isCallable()->no()) {
919: continue;
920: }
921:
922: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
923: }
924:
925: if (count($acceptors) === 0) {
926: throw new ShouldNotHappenException();
927: }
928:
929: return $acceptors;
930: }
931:
932: public function isCloneable(): TrinaryLogic
933: {
934: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
935: }
936:
937: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
938: {
939: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
940: }
941:
942: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
943: {
944: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
945: }
946:
947: public function isNull(): TrinaryLogic
948: {
949: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
950: }
951:
952: public function isConstantValue(): TrinaryLogic
953: {
954: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
955: }
956:
957: public function isConstantScalarValue(): TrinaryLogic
958: {
959: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
960: }
961:
962: public function getConstantScalarTypes(): array
963: {
964: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
965: }
966:
967: public function getConstantScalarValues(): array
968: {
969: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
970: }
971:
972: public function isTrue(): TrinaryLogic
973: {
974: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
975: }
976:
977: public function isFalse(): TrinaryLogic
978: {
979: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
980: }
981:
982: public function isBoolean(): TrinaryLogic
983: {
984: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
985: }
986:
987: public function isFloat(): TrinaryLogic
988: {
989: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
990: }
991:
992: public function isInteger(): TrinaryLogic
993: {
994: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
995: }
996:
997: public function getSmallerType(PhpVersion $phpVersion): Type
998: {
999: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
1000: }
1001:
1002: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
1003: {
1004: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
1005: }
1006:
1007: public function getGreaterType(PhpVersion $phpVersion): Type
1008: {
1009: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
1010: }
1011:
1012: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
1013: {
1014: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
1015: }
1016:
1017: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1018: {
1019: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1020: }
1021:
1022: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1023: {
1024: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1025: }
1026:
1027: public function toBoolean(): BooleanType
1028: {
1029: /** @var BooleanType $type */
1030: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1031:
1032: return $type;
1033: }
1034:
1035: public function toNumber(): Type
1036: {
1037: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
1038:
1039: return $type;
1040: }
1041:
1042: public function toAbsoluteNumber(): Type
1043: {
1044: $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1045:
1046: return $type;
1047: }
1048:
1049: public function toString(): Type
1050: {
1051: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
1052:
1053: return $type;
1054: }
1055:
1056: public function toInteger(): Type
1057: {
1058: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
1059:
1060: return $type;
1061: }
1062:
1063: public function toFloat(): Type
1064: {
1065: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
1066:
1067: return $type;
1068: }
1069:
1070: public function toArray(): Type
1071: {
1072: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
1073:
1074: return $type;
1075: }
1076:
1077: public function toArrayKey(): Type
1078: {
1079: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
1080: }
1081:
1082: public function toCoercedArgumentType(bool $strictTypes): Type
1083: {
1084: return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1085: }
1086:
1087: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1088: {
1089: if ($receivedType instanceof IterableType) {
1090: $receivedType = $receivedType->toArrayOrTraversable();
1091: }
1092:
1093: $types = TemplateTypeMap::createEmpty();
1094: if ($receivedType instanceof UnionType) {
1095: $myTypes = [];
1096: $remainingReceivedTypes = [];
1097: foreach ($receivedType->getTypes() as $receivedInnerType) {
1098: foreach ($this->types as $type) {
1099: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
1100: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
1101: continue 2;
1102: }
1103: $myTypes[] = $type;
1104: }
1105: $remainingReceivedTypes[] = $receivedInnerType;
1106: }
1107: if (count($remainingReceivedTypes) === 0) {
1108: return $types;
1109: }
1110: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
1111: } else {
1112: $myTypes = $this->types;
1113: }
1114:
1115: foreach ($myTypes as $type) {
1116: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
1117: continue;
1118: }
1119: $types = $types->union($type->inferTemplateTypes($receivedType));
1120: }
1121:
1122: if (!$types->isEmpty()) {
1123: return $types;
1124: }
1125:
1126: foreach ($myTypes as $type) {
1127: $types = $types->union($type->inferTemplateTypes($receivedType));
1128: }
1129:
1130: return $types;
1131: }
1132:
1133: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
1134: {
1135: $types = TemplateTypeMap::createEmpty();
1136:
1137: foreach ($this->types as $type) {
1138: $types = $types->union($templateType->inferTemplateTypes($type));
1139: }
1140:
1141: return $types;
1142: }
1143:
1144: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1145: {
1146: $references = [];
1147:
1148: foreach ($this->types as $type) {
1149: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1150: $references[] = $reference;
1151: }
1152: }
1153:
1154: return $references;
1155: }
1156:
1157: public function traverse(callable $cb): Type
1158: {
1159: $types = [];
1160: $changed = false;
1161:
1162: foreach ($this->types as $type) {
1163: $newType = $cb($type);
1164: if ($type !== $newType) {
1165: $changed = true;
1166: }
1167: $types[] = $newType;
1168: }
1169:
1170: if ($changed) {
1171: return TypeCombinator::union(...$types);
1172: }
1173:
1174: return $this;
1175: }
1176:
1177: public function traverseSimultaneously(Type $right, callable $cb): Type
1178: {
1179: $rightTypes = TypeUtils::flattenTypes($right);
1180: $newTypes = [];
1181: $changed = false;
1182: foreach ($this->types as $innerType) {
1183: $candidates = [];
1184: foreach ($rightTypes as $i => $rightType) {
1185: if (!$innerType->isSuperTypeOf($rightType)->yes()) {
1186: continue;
1187: }
1188:
1189: $candidates[] = $rightType;
1190: unset($rightTypes[$i]);
1191: }
1192:
1193: if (count($candidates) === 0) {
1194: $newTypes[] = $innerType;
1195: continue;
1196: }
1197:
1198: $newType = $cb($innerType, TypeCombinator::union(...$candidates));
1199: if ($innerType !== $newType) {
1200: $changed = true;
1201: }
1202:
1203: $newTypes[] = $newType;
1204: }
1205:
1206: if ($changed) {
1207: return TypeCombinator::union(...$newTypes);
1208: }
1209:
1210: return $this;
1211: }
1212:
1213: public function tryRemove(Type $typeToRemove): ?Type
1214: {
1215: return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1216: }
1217:
1218: public function exponentiate(Type $exponent): Type
1219: {
1220: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1221: }
1222:
1223: public function getFiniteTypes(): array
1224: {
1225: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1226: $uniquedTypes = [];
1227: foreach ($types as $type) {
1228: $uniquedTypes[$type->describe(VerbosityLevel::cache())] = $type;
1229: }
1230:
1231: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1232: return [];
1233: }
1234:
1235: return array_values($uniquedTypes);
1236: }
1237:
1238: /**
1239: * @param callable(Type $type): TrinaryLogic $getResult
1240: */
1241: protected function unionResults(callable $getResult): TrinaryLogic
1242: {
1243: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1244: }
1245:
1246: /**
1247: * @param callable(Type $type): TrinaryLogic $getResult
1248: */
1249: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1250: {
1251: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1252: }
1253:
1254: /**
1255: * @param callable(Type $type): Type $getType
1256: */
1257: protected function unionTypes(callable $getType): Type
1258: {
1259: $newTypes = [];
1260: foreach ($this->types as $type) {
1261: $newTypes[] = $getType($type);
1262: }
1263:
1264: return TypeCombinator::union(...$newTypes);
1265: }
1266:
1267: /**
1268: * @template T
1269: * @param callable(Type $type): list<T> $getValues
1270: * @param callable(Type $type): bool $criteria
1271: * @return list<T>
1272: */
1273: protected function pickFromTypes(
1274: callable $getValues,
1275: callable $criteria,
1276: ): array
1277: {
1278: $values = [];
1279: foreach ($this->types as $type) {
1280: $innerValues = $getValues($type);
1281: if ($innerValues === []) {
1282: return [];
1283: }
1284:
1285: foreach ($innerValues as $innerType) {
1286: $values[] = $innerType;
1287: }
1288: }
1289:
1290: return $values;
1291: }
1292:
1293: public function toPhpDocNode(): TypeNode
1294: {
1295: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1296: }
1297:
1298: /**
1299: * @template T
1300: * @param callable(Type $type): list<T> $getValues
1301: * @return list<T>
1302: */
1303: private function notBenevolentPickFromTypes(callable $getValues): array
1304: {
1305: $values = [];
1306: foreach ($this->types as $type) {
1307: $innerValues = $getValues($type);
1308: if ($innerValues === []) {
1309: return [];
1310: }
1311:
1312: foreach ($innerValues as $innerType) {
1313: $values[] = $innerType;
1314: }
1315: }
1316:
1317: return $values;
1318: }
1319:
1320: public function hasTemplateOrLateResolvableType(): bool
1321: {
1322: foreach ($this->types as $type) {
1323: if (!$type->hasTemplateOrLateResolvableType()) {
1324: continue;
1325: }
1326:
1327: return true;
1328: }
1329:
1330: return false;
1331: }
1332:
1333: }
1334: