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: 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 && !$otherType instanceof TemplateType)
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->getIterableKeyType());
643: }
644:
645: public function getLastIterableKeyType(): Type
646: {
647: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
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->getIterableValueType());
658: }
659:
660: public function getLastIterableValueType(): Type
661: {
662: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
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 getEnumCaseObject(): ?EnumCaseObjectType
880: {
881: $cases = $this->getEnumCases();
882:
883: if (count($cases) === 1) {
884: return $cases[0];
885: }
886:
887: return null;
888: }
889:
890: public function isCallable(): TrinaryLogic
891: {
892: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
893: }
894:
895: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
896: {
897: $acceptors = [];
898:
899: foreach ($this->types as $type) {
900: if ($type->isCallable()->no()) {
901: continue;
902: }
903:
904: $acceptors = array_merge($acceptors, $type->getCallableParametersAcceptors($scope));
905: }
906:
907: if (count($acceptors) === 0) {
908: throw new ShouldNotHappenException();
909: }
910:
911: return $acceptors;
912: }
913:
914: public function isCloneable(): TrinaryLogic
915: {
916: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
917: }
918:
919: public function isSmallerThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
920: {
921: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType, $phpVersion));
922: }
923:
924: public function isSmallerThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
925: {
926: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType, $phpVersion));
927: }
928:
929: public function isNull(): TrinaryLogic
930: {
931: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNull());
932: }
933:
934: public function isConstantValue(): TrinaryLogic
935: {
936: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
937: }
938:
939: public function isConstantScalarValue(): TrinaryLogic
940: {
941: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
942: }
943:
944: public function getConstantScalarTypes(): array
945: {
946: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarTypes());
947: }
948:
949: public function getConstantScalarValues(): array
950: {
951: return $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getConstantScalarValues());
952: }
953:
954: public function isTrue(): TrinaryLogic
955: {
956: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
957: }
958:
959: public function isFalse(): TrinaryLogic
960: {
961: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
962: }
963:
964: public function isBoolean(): TrinaryLogic
965: {
966: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
967: }
968:
969: public function isFloat(): TrinaryLogic
970: {
971: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
972: }
973:
974: public function isInteger(): TrinaryLogic
975: {
976: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
977: }
978:
979: public function getSmallerType(PhpVersion $phpVersion): Type
980: {
981: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType($phpVersion));
982: }
983:
984: public function getSmallerOrEqualType(PhpVersion $phpVersion): Type
985: {
986: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType($phpVersion));
987: }
988:
989: public function getGreaterType(PhpVersion $phpVersion): Type
990: {
991: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType($phpVersion));
992: }
993:
994: public function getGreaterOrEqualType(PhpVersion $phpVersion): Type
995: {
996: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType($phpVersion));
997: }
998:
999: public function isGreaterThan(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1000: {
1001: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type, $phpVersion));
1002: }
1003:
1004: public function isGreaterThanOrEqual(Type $otherType, PhpVersion $phpVersion): TrinaryLogic
1005: {
1006: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type, $phpVersion));
1007: }
1008:
1009: public function toBoolean(): BooleanType
1010: {
1011: /** @var BooleanType $type */
1012: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
1013:
1014: return $type;
1015: }
1016:
1017: public function toNumber(): Type
1018: {
1019: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
1020:
1021: return $type;
1022: }
1023:
1024: public function toAbsoluteNumber(): Type
1025: {
1026: $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber());
1027:
1028: return $type;
1029: }
1030:
1031: public function toString(): Type
1032: {
1033: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
1034:
1035: return $type;
1036: }
1037:
1038: public function toInteger(): Type
1039: {
1040: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
1041:
1042: return $type;
1043: }
1044:
1045: public function toFloat(): Type
1046: {
1047: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
1048:
1049: return $type;
1050: }
1051:
1052: public function toArray(): Type
1053: {
1054: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
1055:
1056: return $type;
1057: }
1058:
1059: public function toArrayKey(): Type
1060: {
1061: return $this->unionTypes(static fn (Type $type): Type => $type->toArrayKey());
1062: }
1063:
1064: public function toCoercedArgumentType(bool $strictTypes): Type
1065: {
1066: return $this->unionTypes(static fn (Type $type): Type => $type->toCoercedArgumentType($strictTypes));
1067: }
1068:
1069: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
1070: {
1071: $types = TemplateTypeMap::createEmpty();
1072: if ($receivedType instanceof UnionType) {
1073: $myTypes = [];
1074: $remainingReceivedTypes = [];
1075: foreach ($receivedType->getTypes() as $receivedInnerType) {
1076: foreach ($this->types as $type) {
1077: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
1078: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
1079: continue 2;
1080: }
1081: $myTypes[] = $type;
1082: }
1083: $remainingReceivedTypes[] = $receivedInnerType;
1084: }
1085: if (count($remainingReceivedTypes) === 0) {
1086: return $types;
1087: }
1088: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
1089: } else {
1090: $myTypes = $this->types;
1091: }
1092:
1093: foreach ($myTypes as $type) {
1094: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
1095: continue;
1096: }
1097: $types = $types->union($type->inferTemplateTypes($receivedType));
1098: }
1099:
1100: if (!$types->isEmpty()) {
1101: return $types;
1102: }
1103:
1104: foreach ($myTypes as $type) {
1105: $types = $types->union($type->inferTemplateTypes($receivedType));
1106: }
1107:
1108: return $types;
1109: }
1110:
1111: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
1112: {
1113: $types = TemplateTypeMap::createEmpty();
1114:
1115: foreach ($this->types as $type) {
1116: $types = $types->union($templateType->inferTemplateTypes($type));
1117: }
1118:
1119: return $types;
1120: }
1121:
1122: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
1123: {
1124: $references = [];
1125:
1126: foreach ($this->types as $type) {
1127: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
1128: $references[] = $reference;
1129: }
1130: }
1131:
1132: return $references;
1133: }
1134:
1135: public function traverse(callable $cb): Type
1136: {
1137: $types = [];
1138: $changed = false;
1139:
1140: foreach ($this->types as $type) {
1141: $newType = $cb($type);
1142: if ($type !== $newType) {
1143: $changed = true;
1144: }
1145: $types[] = $newType;
1146: }
1147:
1148: if ($changed) {
1149: return TypeCombinator::union(...$types);
1150: }
1151:
1152: return $this;
1153: }
1154:
1155: public function traverseSimultaneously(Type $right, callable $cb): Type
1156: {
1157: $rightTypes = TypeUtils::flattenTypes($right);
1158: $newTypes = [];
1159: $changed = false;
1160: foreach ($this->types as $innerType) {
1161: $candidates = [];
1162: foreach ($rightTypes as $i => $rightType) {
1163: if (!$innerType->isSuperTypeOf($rightType)->yes()) {
1164: continue;
1165: }
1166:
1167: $candidates[] = $rightType;
1168: unset($rightTypes[$i]);
1169: }
1170:
1171: if (count($candidates) === 0) {
1172: $newTypes[] = $innerType;
1173: continue;
1174: }
1175:
1176: $newType = $cb($innerType, TypeCombinator::union(...$candidates));
1177: if ($innerType !== $newType) {
1178: $changed = true;
1179: }
1180:
1181: $newTypes[] = $newType;
1182: }
1183:
1184: if ($changed) {
1185: return TypeCombinator::union(...$newTypes);
1186: }
1187:
1188: return $this;
1189: }
1190:
1191: public function tryRemove(Type $typeToRemove): ?Type
1192: {
1193: return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1194: }
1195:
1196: public function exponentiate(Type $exponent): Type
1197: {
1198: return $this->unionTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1199: }
1200:
1201: public function getFiniteTypes(): array
1202: {
1203: $types = $this->notBenevolentPickFromTypes(static fn (Type $type) => $type->getFiniteTypes());
1204: $uniquedTypes = [];
1205: foreach ($types as $type) {
1206: $uniquedTypes[$type->describe(VerbosityLevel::cache())] = $type;
1207: }
1208:
1209: if (count($uniquedTypes) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1210: return [];
1211: }
1212:
1213: return array_values($uniquedTypes);
1214: }
1215:
1216: /**
1217: * @param callable(Type $type): TrinaryLogic $getResult
1218: */
1219: protected function unionResults(callable $getResult): TrinaryLogic
1220: {
1221: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1222: }
1223:
1224: /**
1225: * @param callable(Type $type): TrinaryLogic $getResult
1226: */
1227: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
1228: {
1229: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
1230: }
1231:
1232: /**
1233: * @param callable(Type $type): Type $getType
1234: */
1235: protected function unionTypes(callable $getType): Type
1236: {
1237: $newTypes = [];
1238: foreach ($this->types as $type) {
1239: $newTypes[] = $getType($type);
1240: }
1241:
1242: return TypeCombinator::union(...$newTypes);
1243: }
1244:
1245: /**
1246: * @template T
1247: * @param callable(Type $type): list<T> $getValues
1248: * @param callable(Type $type): bool $criteria
1249: * @return list<T>
1250: */
1251: protected function pickFromTypes(
1252: callable $getValues,
1253: callable $criteria,
1254: ): array
1255: {
1256: $values = [];
1257: foreach ($this->types as $type) {
1258: $innerValues = $getValues($type);
1259: if ($innerValues === []) {
1260: return [];
1261: }
1262:
1263: foreach ($innerValues as $innerType) {
1264: $values[] = $innerType;
1265: }
1266: }
1267:
1268: return $values;
1269: }
1270:
1271: public function toPhpDocNode(): TypeNode
1272: {
1273: return new UnionTypeNode(array_map(static fn (Type $type) => $type->toPhpDocNode(), $this->getSortedTypes()));
1274: }
1275:
1276: /**
1277: * @template T
1278: * @param callable(Type $type): list<T> $getValues
1279: * @return list<T>
1280: */
1281: private function notBenevolentPickFromTypes(callable $getValues): array
1282: {
1283: $values = [];
1284: foreach ($this->types as $type) {
1285: $innerValues = $getValues($type);
1286: if ($innerValues === []) {
1287: return [];
1288: }
1289:
1290: foreach ($innerValues as $innerType) {
1291: $values[] = $innerType;
1292: }
1293: }
1294:
1295: return $values;
1296: }
1297:
1298: public function hasTemplateOrLateResolvableType(): bool
1299: {
1300: foreach ($this->types as $type) {
1301: if (!$type->hasTemplateOrLateResolvableType()) {
1302: continue;
1303: }
1304:
1305: return true;
1306: }
1307:
1308: return false;
1309: }
1310:
1311: }
1312: