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