1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use DateTime;
6: use DateTimeImmutable;
7: use DateTimeInterface;
8: use PHPStan\Reflection\ClassMemberAccessAnswerer;
9: use PHPStan\Reflection\ConstantReflection;
10: use PHPStan\Reflection\MethodReflection;
11: use PHPStan\Reflection\ParametersAcceptor;
12: use PHPStan\Reflection\PropertyReflection;
13: use PHPStan\Reflection\Type\UnionTypeUnresolvedMethodPrototypeReflection;
14: use PHPStan\Reflection\Type\UnionTypeUnresolvedPropertyPrototypeReflection;
15: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
16: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
17: use PHPStan\ShouldNotHappenException;
18: use PHPStan\TrinaryLogic;
19: use PHPStan\Type\Constant\ConstantBooleanType;
20: use PHPStan\Type\Generic\GenericClassStringType;
21: use PHPStan\Type\Generic\TemplateMixedType;
22: use PHPStan\Type\Generic\TemplateType;
23: use PHPStan\Type\Generic\TemplateTypeMap;
24: use PHPStan\Type\Generic\TemplateTypeVariance;
25: use PHPStan\Type\Generic\TemplateUnionType;
26: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
27: use function array_map;
28: use function count;
29: use function implode;
30: use function sprintf;
31: use function strpos;
32:
33: /** @api */
34: class UnionType implements CompoundType
35: {
36:
37: use NonGeneralizableTypeTrait;
38:
39: private bool $sortedTypes = false;
40:
41: /**
42: * @api
43: * @param Type[] $types
44: */
45: public function __construct(private array $types)
46: {
47: $throwException = static function () use ($types): void {
48: throw new ShouldNotHappenException(sprintf(
49: 'Cannot create %s with: %s',
50: self::class,
51: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
52: ));
53: };
54: if (count($types) < 2) {
55: $throwException();
56: }
57: foreach ($types as $type) {
58: if (!($type instanceof UnionType)) {
59: continue;
60: }
61: if ($type instanceof TemplateType) {
62: continue;
63: }
64:
65: $throwException();
66: }
67: }
68:
69: /**
70: * @return Type[]
71: */
72: public function getTypes(): array
73: {
74: return $this->types;
75: }
76:
77: /**
78: * @return Type[]
79: */
80: private function getSortedTypes(): array
81: {
82: if ($this->sortedTypes) {
83: return $this->types;
84: }
85:
86: $this->types = UnionTypeHelper::sortTypes($this->types);
87: $this->sortedTypes = true;
88:
89: return $this->types;
90: }
91:
92: /**
93: * @return string[]
94: */
95: public function getReferencedClasses(): array
96: {
97: return UnionTypeHelper::getReferencedClasses($this->getTypes());
98: }
99:
100: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
101: {
102: if (
103: $type->equals(new ObjectType(DateTimeInterface::class))
104: && $this->accepts(
105: new UnionType([new ObjectType(DateTime::class), new ObjectType(DateTimeImmutable::class)]),
106: $strictTypes,
107: )->yes()
108: ) {
109: return TrinaryLogic::createYes();
110: }
111:
112: if ($type instanceof CompoundType && !$type instanceof CallableType && !$type instanceof TemplateType && !$type instanceof IntersectionType) {
113: return $type->isAcceptedBy($this, $strictTypes);
114: }
115:
116: $result = TrinaryLogic::createNo()->lazyOr($this->getTypes(), static fn (Type $innerType) => $innerType->accepts($type, $strictTypes));
117: if ($result->yes()) {
118: return $result;
119: }
120:
121: if ($type instanceof TemplateUnionType) {
122: return $result->or($type->isAcceptedBy($this, $strictTypes));
123: }
124:
125: return $result;
126: }
127:
128: public function isSuperTypeOf(Type $otherType): TrinaryLogic
129: {
130: if (
131: ($otherType instanceof self && !$otherType instanceof TemplateUnionType)
132: || $otherType instanceof IterableType
133: || $otherType instanceof NeverType
134: || $otherType instanceof ConditionalType
135: || $otherType instanceof ConditionalTypeForParameter
136: || $otherType instanceof IntegerRangeType
137: ) {
138: return $otherType->isSubTypeOf($this);
139: }
140:
141: $result = TrinaryLogic::createNo()->lazyOr($this->getTypes(), static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType));
142: if ($result->yes()) {
143: return $result;
144: }
145:
146: if ($otherType instanceof TemplateUnionType) {
147: return $result->or($otherType->isSubTypeOf($this));
148: }
149:
150: return $result;
151: }
152:
153: public function isSubTypeOf(Type $otherType): TrinaryLogic
154: {
155: return TrinaryLogic::lazyExtremeIdentity($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType));
156: }
157:
158: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
159: {
160: return TrinaryLogic::lazyExtremeIdentity($this->getTypes(), static fn (Type $innerType) => $acceptingType->accepts($innerType, $strictTypes));
161: }
162:
163: public function equals(Type $type): bool
164: {
165: if (!$type instanceof static) {
166: return false;
167: }
168:
169: if (count($this->types) !== count($type->types)) {
170: return false;
171: }
172:
173: $otherTypes = $type->types;
174: foreach ($this->types as $innerType) {
175: $match = false;
176: foreach ($otherTypes as $i => $otherType) {
177: if (!$innerType->equals($otherType)) {
178: continue;
179: }
180:
181: $match = true;
182: unset($otherTypes[$i]);
183: break;
184: }
185:
186: if (!$match) {
187: return false;
188: }
189: }
190:
191: return count($otherTypes) === 0;
192: }
193:
194: public function describe(VerbosityLevel $level): string
195: {
196: $joinTypes = static function (array $types) use ($level): string {
197: $typeNames = [];
198: foreach ($types as $i => $type) {
199: if ($type instanceof ClosureType || $type instanceof CallableType || $type instanceof TemplateUnionType) {
200: $typeNames[] = sprintf('(%s)', $type->describe($level));
201: } elseif ($type instanceof TemplateType) {
202: $isLast = $i >= count($types) - 1;
203: $bound = $type->getBound();
204: if (
205: !$isLast
206: && ($level->isTypeOnly() || $level->isValue())
207: && !($bound instanceof MixedType && $bound->getSubtractedType() === null && !$bound instanceof TemplateMixedType)
208: ) {
209: $typeNames[] = sprintf('(%s)', $type->describe($level));
210: } else {
211: $typeNames[] = $type->describe($level);
212: }
213: } elseif ($type instanceof IntersectionType) {
214: $intersectionDescription = $type->describe($level);
215: if (strpos($intersectionDescription, '&') !== false) {
216: $typeNames[] = sprintf('(%s)', $type->describe($level));
217: } else {
218: $typeNames[] = $intersectionDescription;
219: }
220: } else {
221: $typeNames[] = $type->describe($level);
222: }
223: }
224:
225: return implode('|', $typeNames);
226: };
227:
228: return $level->handle(
229: function () use ($joinTypes): string {
230: $types = TypeCombinator::union(...array_map(static function (Type $type): Type {
231: if (
232: $type instanceof ConstantType
233: && !$type instanceof ConstantBooleanType
234: ) {
235: return $type->generalize(GeneralizePrecision::lessSpecific());
236: }
237:
238: return $type;
239: }, $this->getSortedTypes()));
240:
241: if ($types instanceof UnionType) {
242: return $joinTypes($types->getSortedTypes());
243: }
244:
245: return $joinTypes([$types]);
246: },
247: fn (): string => $joinTypes($this->getSortedTypes()),
248: );
249: }
250:
251: /**
252: * @param callable(Type $type): TrinaryLogic $canCallback
253: * @param callable(Type $type): TrinaryLogic $hasCallback
254: */
255: private function hasInternal(
256: callable $canCallback,
257: callable $hasCallback,
258: ): TrinaryLogic
259: {
260: return TrinaryLogic::lazyExtremeIdentity($this->types, static function (Type $type) use ($canCallback, $hasCallback): TrinaryLogic {
261: if ($canCallback($type)->no()) {
262: return TrinaryLogic::createNo();
263: }
264:
265: return $hasCallback($type);
266: });
267: }
268:
269: /**
270: * @template TObject of object
271: * @param callable(Type $type): TrinaryLogic $hasCallback
272: * @param callable(Type $type): TObject $getCallback
273: * @return TObject
274: */
275: private function getInternal(
276: callable $hasCallback,
277: callable $getCallback,
278: ): object
279: {
280: /** @var TrinaryLogic|null $result */
281: $result = null;
282:
283: /** @var TObject|null $object */
284: $object = null;
285: foreach ($this->types as $type) {
286: $has = $hasCallback($type);
287: if (!$has->yes()) {
288: continue;
289: }
290: if ($result !== null && $result->compareTo($has) !== $has) {
291: continue;
292: }
293:
294: $get = $getCallback($type);
295: $result = $has;
296: $object = $get;
297: }
298:
299: if ($object === null) {
300: throw new ShouldNotHappenException();
301: }
302:
303: return $object;
304: }
305:
306: public function canAccessProperties(): TrinaryLogic
307: {
308: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
309: }
310:
311: public function hasProperty(string $propertyName): TrinaryLogic
312: {
313: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
314: }
315:
316: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
317: {
318: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
319: }
320:
321: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
322: {
323: $propertyPrototypes = [];
324: foreach ($this->types as $type) {
325: if (!$type->hasProperty($propertyName)->yes()) {
326: continue;
327: }
328:
329: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
330: }
331:
332: $propertiesCount = count($propertyPrototypes);
333: if ($propertiesCount === 0) {
334: throw new ShouldNotHappenException();
335: }
336:
337: if ($propertiesCount === 1) {
338: return $propertyPrototypes[0];
339: }
340:
341: return new UnionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
342: }
343:
344: public function canCallMethods(): TrinaryLogic
345: {
346: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
347: }
348:
349: public function hasMethod(string $methodName): TrinaryLogic
350: {
351: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
352: }
353:
354: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): MethodReflection
355: {
356: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
357: }
358:
359: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
360: {
361: $methodPrototypes = [];
362: foreach ($this->types as $type) {
363: if (!$type->hasMethod($methodName)->yes()) {
364: continue;
365: }
366:
367: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
368: }
369:
370: $methodsCount = count($methodPrototypes);
371: if ($methodsCount === 0) {
372: throw new ShouldNotHappenException();
373: }
374:
375: if ($methodsCount === 1) {
376: return $methodPrototypes[0];
377: }
378:
379: return new UnionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
380: }
381:
382: public function canAccessConstants(): TrinaryLogic
383: {
384: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
385: }
386:
387: public function hasConstant(string $constantName): TrinaryLogic
388: {
389: return $this->hasInternal(
390: static fn (Type $type): TrinaryLogic => $type->canAccessConstants(),
391: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
392: );
393: }
394:
395: public function getConstant(string $constantName): ConstantReflection
396: {
397: return $this->getInternal(
398: static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName),
399: static fn (Type $type): ConstantReflection => $type->getConstant($constantName),
400: );
401: }
402:
403: public function isIterable(): TrinaryLogic
404: {
405: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
406: }
407:
408: public function isIterableAtLeastOnce(): TrinaryLogic
409: {
410: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
411: }
412:
413: public function getIterableKeyType(): Type
414: {
415: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableKeyType());
416: }
417:
418: public function getIterableValueType(): Type
419: {
420: return $this->unionTypes(static fn (Type $type): Type => $type->getIterableValueType());
421: }
422:
423: public function isArray(): TrinaryLogic
424: {
425: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isArray());
426: }
427:
428: public function isOversizedArray(): TrinaryLogic
429: {
430: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
431: }
432:
433: public function isString(): TrinaryLogic
434: {
435: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isString());
436: }
437:
438: public function isNumericString(): TrinaryLogic
439: {
440: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
441: }
442:
443: public function isNonEmptyString(): TrinaryLogic
444: {
445: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
446: }
447:
448: public function isNonFalsyString(): TrinaryLogic
449: {
450: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
451: }
452:
453: public function isLiteralString(): TrinaryLogic
454: {
455: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
456: }
457:
458: public function isOffsetAccessible(): TrinaryLogic
459: {
460: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
461: }
462:
463: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
464: {
465: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
466: }
467:
468: public function getOffsetValueType(Type $offsetType): Type
469: {
470: $types = [];
471: foreach ($this->types as $innerType) {
472: $valueType = $innerType->getOffsetValueType($offsetType);
473: if ($valueType instanceof ErrorType) {
474: continue;
475: }
476:
477: $types[] = $valueType;
478: }
479:
480: if (count($types) === 0) {
481: return new ErrorType();
482: }
483:
484: return TypeCombinator::union(...$types);
485: }
486:
487: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
488: {
489: return $this->unionTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
490: }
491:
492: public function unsetOffset(Type $offsetType): Type
493: {
494: return $this->unionTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
495: }
496:
497: public function isCallable(): TrinaryLogic
498: {
499: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
500: }
501:
502: /**
503: * @return ParametersAcceptor[]
504: */
505: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
506: {
507: foreach ($this->types as $type) {
508: if ($type->isCallable()->no()) {
509: continue;
510: }
511:
512: return $type->getCallableParametersAcceptors($scope);
513: }
514:
515: throw new ShouldNotHappenException();
516: }
517:
518: public function isCloneable(): TrinaryLogic
519: {
520: return $this->unionResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
521: }
522:
523: public function isSmallerThan(Type $otherType): TrinaryLogic
524: {
525: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType));
526: }
527:
528: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
529: {
530: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType));
531: }
532:
533: public function getSmallerType(): Type
534: {
535: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerType());
536: }
537:
538: public function getSmallerOrEqualType(): Type
539: {
540: return $this->unionTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType());
541: }
542:
543: public function getGreaterType(): Type
544: {
545: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterType());
546: }
547:
548: public function getGreaterOrEqualType(): Type
549: {
550: return $this->unionTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType());
551: }
552:
553: public function isGreaterThan(Type $otherType): TrinaryLogic
554: {
555: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type));
556: }
557:
558: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
559: {
560: return $this->notBenevolentUnionResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type));
561: }
562:
563: public function toBoolean(): BooleanType
564: {
565: /** @var BooleanType $type */
566: $type = $this->unionTypes(static fn (Type $type): BooleanType => $type->toBoolean());
567:
568: return $type;
569: }
570:
571: public function toNumber(): Type
572: {
573: $type = $this->unionTypes(static fn (Type $type): Type => $type->toNumber());
574:
575: return $type;
576: }
577:
578: public function toString(): Type
579: {
580: $type = $this->unionTypes(static fn (Type $type): Type => $type->toString());
581:
582: return $type;
583: }
584:
585: public function toInteger(): Type
586: {
587: $type = $this->unionTypes(static fn (Type $type): Type => $type->toInteger());
588:
589: return $type;
590: }
591:
592: public function toFloat(): Type
593: {
594: $type = $this->unionTypes(static fn (Type $type): Type => $type->toFloat());
595:
596: return $type;
597: }
598:
599: public function toArray(): Type
600: {
601: $type = $this->unionTypes(static fn (Type $type): Type => $type->toArray());
602:
603: return $type;
604: }
605:
606: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
607: {
608: $types = TemplateTypeMap::createEmpty();
609: if ($receivedType instanceof UnionType) {
610: $myTypes = [];
611: $remainingReceivedTypes = [];
612: foreach ($receivedType->getTypes() as $receivedInnerType) {
613: foreach ($this->types as $type) {
614: if ($type->isSuperTypeOf($receivedInnerType)->yes()) {
615: $types = $types->union($type->inferTemplateTypes($receivedInnerType));
616: continue 2;
617: }
618: $myTypes[] = $type;
619: }
620: $remainingReceivedTypes[] = $receivedInnerType;
621: }
622: if (count($remainingReceivedTypes) === 0) {
623: return $types;
624: }
625: $receivedType = TypeCombinator::union(...$remainingReceivedTypes);
626: } else {
627: $myTypes = $this->types;
628: }
629:
630: $myTemplateTypes = [];
631: foreach ($myTypes as $type) {
632: if ($type instanceof TemplateType || ($type instanceof GenericClassStringType && $type->getGenericType() instanceof TemplateType)) {
633: $myTemplateTypes[] = $type;
634: continue;
635: }
636: $types = $types->union($type->inferTemplateTypes($receivedType));
637: }
638:
639: if (!$types->isEmpty()) {
640: return $types;
641: }
642:
643: foreach ($myTypes as $type) {
644: $types = $types->union($type->inferTemplateTypes($receivedType));
645: }
646:
647: return $types;
648: }
649:
650: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
651: {
652: $types = TemplateTypeMap::createEmpty();
653:
654: foreach ($this->types as $type) {
655: $types = $types->union($templateType->inferTemplateTypes($type));
656: }
657:
658: return $types;
659: }
660:
661: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
662: {
663: $references = [];
664:
665: foreach ($this->types as $type) {
666: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
667: $references[] = $reference;
668: }
669: }
670:
671: return $references;
672: }
673:
674: public function traverse(callable $cb): Type
675: {
676: $types = [];
677: $changed = false;
678:
679: foreach ($this->types as $type) {
680: $newType = $cb($type);
681: if ($type !== $newType) {
682: $changed = true;
683: }
684: $types[] = $newType;
685: }
686:
687: if ($changed) {
688: return TypeCombinator::union(...$types);
689: }
690:
691: return $this;
692: }
693:
694: public function tryRemove(Type $typeToRemove): ?Type
695: {
696: return $this->unionTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
697: }
698:
699: /**
700: * @param mixed[] $properties
701: */
702: public static function __set_state(array $properties): Type
703: {
704: return new self($properties['types']);
705: }
706:
707: /**
708: * @param callable(Type $type): TrinaryLogic $getResult
709: */
710: protected function unionResults(callable $getResult): TrinaryLogic
711: {
712: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
713: }
714:
715: /**
716: * @param callable(Type $type): TrinaryLogic $getResult
717: */
718: private function notBenevolentUnionResults(callable $getResult): TrinaryLogic
719: {
720: return TrinaryLogic::lazyExtremeIdentity($this->types, $getResult);
721: }
722:
723: /**
724: * @param callable(Type $type): Type $getType
725: */
726: protected function unionTypes(callable $getType): Type
727: {
728: return TypeCombinator::union(...array_map($getType, $this->types));
729: }
730:
731: }
732: