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