1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\Php\PhpVersion;
6: use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
7: use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
8: use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
9: use PHPStan\PhpDocParser\Ast\Type\TypeNode;
10: use PHPStan\Reflection\ClassMemberAccessAnswerer;
11: use PHPStan\Reflection\ConstantReflection;
12: use PHPStan\Reflection\ExtendedMethodReflection;
13: use PHPStan\Reflection\InitializerExprTypeResolver;
14: use PHPStan\Reflection\PropertyReflection;
15: use PHPStan\Reflection\TrivialParametersAcceptor;
16: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedMethodPrototypeReflection;
17: use PHPStan\Reflection\Type\IntersectionTypeUnresolvedPropertyPrototypeReflection;
18: use PHPStan\Reflection\Type\UnresolvedMethodPrototypeReflection;
19: use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection;
20: use PHPStan\ShouldNotHappenException;
21: use PHPStan\TrinaryLogic;
22: use PHPStan\Type\Accessory\AccessoryArrayListType;
23: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
24: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
25: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
26: use PHPStan\Type\Accessory\AccessoryNumericStringType;
27: use PHPStan\Type\Accessory\AccessoryType;
28: use PHPStan\Type\Accessory\NonEmptyArrayType;
29: use PHPStan\Type\Constant\ConstantIntegerType;
30: use PHPStan\Type\Generic\TemplateType;
31: use PHPStan\Type\Generic\TemplateTypeMap;
32: use PHPStan\Type\Generic\TemplateTypeVariance;
33: use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
34: use PHPStan\Type\Traits\NonRemoveableTypeTrait;
35: use function array_intersect_key;
36: use function array_map;
37: use function array_shift;
38: use function array_unique;
39: use function array_values;
40: use function count;
41: use function implode;
42: use function in_array;
43: use function ksort;
44: use function md5;
45: use function sprintf;
46: use function str_starts_with;
47: use function strlen;
48: use function substr;
49:
50: /** @api */
51: class IntersectionType implements CompoundType
52: {
53:
54: use NonRemoveableTypeTrait;
55: use NonGeneralizableTypeTrait;
56:
57: private bool $sortedTypes = false;
58:
59: /**
60: * @api
61: * @param Type[] $types
62: */
63: public function __construct(private array $types)
64: {
65: if (count($types) < 2) {
66: throw new ShouldNotHappenException(sprintf(
67: 'Cannot create %s with: %s',
68: self::class,
69: implode(', ', array_map(static fn (Type $type): string => $type->describe(VerbosityLevel::value()), $types)),
70: ));
71: }
72: }
73:
74: /**
75: * @return Type[]
76: */
77: public function getTypes(): array
78: {
79: return $this->types;
80: }
81:
82: /**
83: * @return Type[]
84: */
85: private function getSortedTypes(): array
86: {
87: if ($this->sortedTypes) {
88: return $this->types;
89: }
90:
91: $this->types = UnionTypeHelper::sortTypes($this->types);
92: $this->sortedTypes = true;
93:
94: return $this->types;
95: }
96:
97: public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
98: {
99: $types = TemplateTypeMap::createEmpty();
100:
101: foreach ($this->types as $type) {
102: $types = $types->intersect($templateType->inferTemplateTypes($type));
103: }
104:
105: return $types;
106: }
107:
108: public function getReferencedClasses(): array
109: {
110: $classes = [];
111: foreach ($this->types as $type) {
112: foreach ($type->getReferencedClasses() as $className) {
113: $classes[] = $className;
114: }
115: }
116:
117: return $classes;
118: }
119:
120: public function getObjectClassNames(): array
121: {
122: $objectClassNames = [];
123: foreach ($this->types as $type) {
124: $innerObjectClassNames = $type->getObjectClassNames();
125: foreach ($innerObjectClassNames as $innerObjectClassName) {
126: $objectClassNames[] = $innerObjectClassName;
127: }
128: }
129:
130: return array_values(array_unique($objectClassNames));
131: }
132:
133: public function getObjectClassReflections(): array
134: {
135: $reflections = [];
136: foreach ($this->types as $type) {
137: foreach ($type->getObjectClassReflections() as $reflection) {
138: $reflections[] = $reflection;
139: }
140: }
141:
142: return $reflections;
143: }
144:
145: public function getArrays(): array
146: {
147: $arrays = [];
148: foreach ($this->types as $type) {
149: foreach ($type->getArrays() as $array) {
150: $arrays[] = $array;
151: }
152: }
153:
154: return $arrays;
155: }
156:
157: public function getConstantArrays(): array
158: {
159: $constantArrays = [];
160: foreach ($this->types as $type) {
161: foreach ($type->getConstantArrays() as $constantArray) {
162: $constantArrays[] = $constantArray;
163: }
164: }
165:
166: return $constantArrays;
167: }
168:
169: public function getConstantStrings(): array
170: {
171: $strings = [];
172: foreach ($this->types as $type) {
173: foreach ($type->getConstantStrings() as $string) {
174: $strings[] = $string;
175: }
176: }
177:
178: return $strings;
179: }
180:
181: public function accepts(Type $type, bool $strictTypes): TrinaryLogic
182: {
183: return $this->acceptsWithReason($type, $strictTypes)->result;
184: }
185:
186: public function acceptsWithReason(Type $otherType, bool $strictTypes): AcceptsResult
187: {
188: $result = AcceptsResult::createYes();
189: foreach ($this->types as $type) {
190: $result = $result->and($type->acceptsWithReason($otherType, $strictTypes));
191: }
192:
193: if (!$result->yes()) {
194: $isList = $otherType->isList();
195: $reasons = $result->reasons;
196: $verbosity = VerbosityLevel::getRecommendedLevelByType($this, $otherType);
197: if ($this->isList()->yes() && !$isList->yes()) {
198: $reasons[] = sprintf(
199: '%s %s a list.',
200: $otherType->describe($verbosity),
201: $isList->no() ? 'is not' : 'might not be',
202: );
203: }
204:
205: $isNonEmpty = $otherType->isIterableAtLeastOnce();
206: if ($this->isIterableAtLeastOnce()->yes() && !$isNonEmpty->yes()) {
207: $reasons[] = sprintf(
208: '%s %s empty.',
209: $otherType->describe($verbosity),
210: $isNonEmpty->no() ? 'is' : 'might be',
211: );
212: }
213:
214: if (count($reasons) > 0) {
215: return new AcceptsResult($result->result, $reasons);
216: }
217: }
218:
219: return $result;
220: }
221:
222: public function isSuperTypeOf(Type $otherType): TrinaryLogic
223: {
224: if ($otherType instanceof IntersectionType && $this->equals($otherType)) {
225: return TrinaryLogic::createYes();
226: }
227:
228: if ($otherType instanceof NeverType) {
229: return TrinaryLogic::createYes();
230: }
231:
232: return TrinaryLogic::createYes()->lazyAnd($this->getTypes(), static fn (Type $innerType) => $innerType->isSuperTypeOf($otherType));
233: }
234:
235: public function isSubTypeOf(Type $otherType): TrinaryLogic
236: {
237: if (($otherType instanceof self || $otherType instanceof UnionType) && !$otherType instanceof TemplateType) {
238: return $otherType->isSuperTypeOf($this);
239: }
240:
241: $result = TrinaryLogic::lazyMaxMin($this->getTypes(), static fn (Type $innerType) => $otherType->isSuperTypeOf($innerType));
242: if ($this->isOversizedArray()->yes()) {
243: if (!$result->no()) {
244: return TrinaryLogic::createYes();
245: }
246: }
247:
248: return $result;
249: }
250:
251: public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLogic
252: {
253: return $this->isAcceptedWithReasonBy($acceptingType, $strictTypes)->result;
254: }
255:
256: public function isAcceptedWithReasonBy(Type $acceptingType, bool $strictTypes): AcceptsResult
257: {
258: $result = AcceptsResult::maxMin(...array_map(static fn (Type $innerType) => $acceptingType->acceptsWithReason($innerType, $strictTypes), $this->types));
259: if ($this->isOversizedArray()->yes()) {
260: if (!$result->no()) {
261: return AcceptsResult::createYes();
262: }
263: }
264:
265: return $result;
266: }
267:
268: public function equals(Type $type): bool
269: {
270: if (!$type instanceof static) {
271: return false;
272: }
273:
274: if (count($this->types) !== count($type->types)) {
275: return false;
276: }
277:
278: $otherTypes = $type->types;
279: foreach ($this->types as $innerType) {
280: $match = false;
281: foreach ($otherTypes as $i => $otherType) {
282: if (!$innerType->equals($otherType)) {
283: continue;
284: }
285:
286: $match = true;
287: unset($otherTypes[$i]);
288: break;
289: }
290:
291: if (!$match) {
292: return false;
293: }
294: }
295:
296: return count($otherTypes) === 0;
297: }
298:
299: public function describe(VerbosityLevel $level): string
300: {
301: return $level->handle(
302: function () use ($level): string {
303: $typeNames = [];
304: foreach ($this->getSortedTypes() as $type) {
305: if ($type instanceof AccessoryType) {
306: continue;
307: }
308: $typeNames[] = $type->generalize(GeneralizePrecision::lessSpecific())->describe($level);
309: }
310:
311: return implode('&', $typeNames);
312: },
313: fn (): string => $this->describeItself($level, true),
314: fn (): string => $this->describeItself($level, false),
315: );
316: }
317:
318: private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes): string
319: {
320: $baseTypes = [];
321: $typesToDescribe = [];
322: $skipTypeNames = [];
323:
324: $nonEmptyStr = false;
325: $nonFalsyStr = false;
326: foreach ($this->getSortedTypes() as $i => $type) {
327: if ($type instanceof AccessoryNonEmptyStringType
328: || $type instanceof AccessoryLiteralStringType
329: || $type instanceof AccessoryNumericStringType
330: || $type instanceof AccessoryNonFalsyStringType
331: ) {
332: if ($type instanceof AccessoryNonFalsyStringType) {
333: $nonFalsyStr = true;
334: }
335: if ($type instanceof AccessoryNonEmptyStringType) {
336: $nonEmptyStr = true;
337: }
338: if ($nonEmptyStr && $nonFalsyStr) {
339: // prevent redundant 'non-empty-string&non-falsy-string'
340: foreach ($typesToDescribe as $key => $typeToDescribe) {
341: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
342: continue;
343: }
344:
345: unset($typesToDescribe[$key]);
346: }
347: }
348:
349: $typesToDescribe[$i] = $type;
350: $skipTypeNames[] = 'string';
351: continue;
352: }
353: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
354: $typesToDescribe[$i] = $type;
355: $skipTypeNames[] = 'array';
356: continue;
357: }
358:
359: if ($type instanceof CallableType && $type->isCommonCallable()) {
360: $typesToDescribe[$i] = $type;
361: $skipTypeNames[] = 'object';
362: $skipTypeNames[] = 'string';
363: continue;
364: }
365:
366: if (!$type instanceof AccessoryType) {
367: $baseTypes[$i] = $type;
368: continue;
369: }
370:
371: if ($skipAccessoryTypes) {
372: continue;
373: }
374:
375: $typesToDescribe[$i] = $type;
376: }
377:
378: $describedTypes = [];
379: foreach ($baseTypes as $i => $type) {
380: $typeDescription = $type->describe($level);
381:
382: if (in_array($typeDescription, ['object', 'string'], true) && in_array($typeDescription, $skipTypeNames, true)) {
383: foreach ($typesToDescribe as $j => $typeToDescribe) {
384: if ($typeToDescribe instanceof CallableType && $typeToDescribe->isCommonCallable()) {
385: $describedTypes[$i] = 'callable-' . $typeDescription;
386: unset($typesToDescribe[$j]);
387: continue 2;
388: }
389: }
390: }
391:
392: if (
393: str_starts_with($typeDescription, 'array<')
394: && in_array('array', $skipTypeNames, true)
395: ) {
396: $nonEmpty = false;
397: $typeName = 'array';
398: foreach ($typesToDescribe as $j => $typeToDescribe) {
399: if (
400: $typeToDescribe instanceof AccessoryArrayListType
401: && substr($typeDescription, 0, strlen('array<int<0, max>, ')) === 'array<int<0, max>, '
402: ) {
403: $typeName = 'list';
404: $typeDescription = 'array<' . substr($typeDescription, strlen('array<int<0, max>, '));
405: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
406: $nonEmpty = true;
407: } else {
408: continue;
409: }
410:
411: unset($typesToDescribe[$j]);
412: }
413:
414: if ($nonEmpty) {
415: $typeName = 'non-empty-' . $typeName;
416: }
417:
418: $describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<'));
419: continue;
420: }
421:
422: if (in_array($typeDescription, $skipTypeNames, true)) {
423: continue;
424: }
425:
426: $describedTypes[$i] = $type->describe($level);
427: }
428:
429: foreach ($typesToDescribe as $i => $typeToDescribe) {
430: $describedTypes[$i] = $typeToDescribe->describe($level);
431: }
432:
433: ksort($describedTypes);
434:
435: return implode('&', $describedTypes);
436: }
437:
438: public function getTemplateType(string $ancestorClassName, string $templateTypeName): Type
439: {
440: return $this->intersectTypes(static fn (Type $type): Type => $type->getTemplateType($ancestorClassName, $templateTypeName));
441: }
442:
443: public function isObject(): TrinaryLogic
444: {
445: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isObject());
446: }
447:
448: public function isEnum(): TrinaryLogic
449: {
450: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isEnum());
451: }
452:
453: public function canAccessProperties(): TrinaryLogic
454: {
455: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessProperties());
456: }
457:
458: public function hasProperty(string $propertyName): TrinaryLogic
459: {
460: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasProperty($propertyName));
461: }
462:
463: public function getProperty(string $propertyName, ClassMemberAccessAnswerer $scope): PropertyReflection
464: {
465: return $this->getUnresolvedPropertyPrototype($propertyName, $scope)->getTransformedProperty();
466: }
467:
468: public function getUnresolvedPropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
469: {
470: $propertyPrototypes = [];
471: foreach ($this->types as $type) {
472: if (!$type->hasProperty($propertyName)->yes()) {
473: continue;
474: }
475:
476: $propertyPrototypes[] = $type->getUnresolvedPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
477: }
478:
479: $propertiesCount = count($propertyPrototypes);
480: if ($propertiesCount === 0) {
481: throw new ShouldNotHappenException();
482: }
483:
484: if ($propertiesCount === 1) {
485: return $propertyPrototypes[0];
486: }
487:
488: return new IntersectionTypeUnresolvedPropertyPrototypeReflection($propertyName, $propertyPrototypes);
489: }
490:
491: public function canCallMethods(): TrinaryLogic
492: {
493: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canCallMethods());
494: }
495:
496: public function hasMethod(string $methodName): TrinaryLogic
497: {
498: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasMethod($methodName));
499: }
500:
501: public function getMethod(string $methodName, ClassMemberAccessAnswerer $scope): ExtendedMethodReflection
502: {
503: return $this->getUnresolvedMethodPrototype($methodName, $scope)->getTransformedMethod();
504: }
505:
506: public function getUnresolvedMethodPrototype(string $methodName, ClassMemberAccessAnswerer $scope): UnresolvedMethodPrototypeReflection
507: {
508: $methodPrototypes = [];
509: foreach ($this->types as $type) {
510: if (!$type->hasMethod($methodName)->yes()) {
511: continue;
512: }
513:
514: $methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
515: }
516:
517: $methodsCount = count($methodPrototypes);
518: if ($methodsCount === 0) {
519: throw new ShouldNotHappenException();
520: }
521:
522: if ($methodsCount === 1) {
523: return $methodPrototypes[0];
524: }
525:
526: return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
527: }
528:
529: public function canAccessConstants(): TrinaryLogic
530: {
531: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
532: }
533:
534: public function hasConstant(string $constantName): TrinaryLogic
535: {
536: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasConstant($constantName));
537: }
538:
539: public function getConstant(string $constantName): ConstantReflection
540: {
541: foreach ($this->types as $type) {
542: if ($type->hasConstant($constantName)->yes()) {
543: return $type->getConstant($constantName);
544: }
545: }
546:
547: throw new ShouldNotHappenException();
548: }
549:
550: public function isIterable(): TrinaryLogic
551: {
552: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterable());
553: }
554:
555: public function isIterableAtLeastOnce(): TrinaryLogic
556: {
557: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isIterableAtLeastOnce());
558: }
559:
560: public function getArraySize(): Type
561: {
562: return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
563: }
564:
565: public function getIterableKeyType(): Type
566: {
567: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableKeyType());
568: }
569:
570: public function getFirstIterableKeyType(): Type
571: {
572: return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableKeyType());
573: }
574:
575: public function getLastIterableKeyType(): Type
576: {
577: return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableKeyType());
578: }
579:
580: public function getIterableValueType(): Type
581: {
582: return $this->intersectTypes(static fn (Type $type): Type => $type->getIterableValueType());
583: }
584:
585: public function getFirstIterableValueType(): Type
586: {
587: return $this->intersectTypes(static fn (Type $type): Type => $type->getFirstIterableValueType());
588: }
589:
590: public function getLastIterableValueType(): Type
591: {
592: return $this->intersectTypes(static fn (Type $type): Type => $type->getLastIterableValueType());
593: }
594:
595: public function isArray(): TrinaryLogic
596: {
597: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isArray());
598: }
599:
600: public function isConstantArray(): TrinaryLogic
601: {
602: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantArray());
603: }
604:
605: public function isOversizedArray(): TrinaryLogic
606: {
607: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOversizedArray());
608: }
609:
610: public function isList(): TrinaryLogic
611: {
612: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isList());
613: }
614:
615: public function isString(): TrinaryLogic
616: {
617: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isString());
618: }
619:
620: public function isNumericString(): TrinaryLogic
621: {
622: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNumericString());
623: }
624:
625: public function isNonEmptyString(): TrinaryLogic
626: {
627: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonEmptyString());
628: }
629:
630: public function isNonFalsyString(): TrinaryLogic
631: {
632: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNonFalsyString());
633: }
634:
635: public function isLiteralString(): TrinaryLogic
636: {
637: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isLiteralString());
638: }
639:
640: public function isClassStringType(): TrinaryLogic
641: {
642: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isClassStringType());
643: }
644:
645: public function getClassStringObjectType(): Type
646: {
647: return $this->intersectTypes(static fn (Type $type): Type => $type->getClassStringObjectType());
648: }
649:
650: public function getObjectTypeOrClassStringObjectType(): Type
651: {
652: return $this->intersectTypes(static fn (Type $type): Type => $type->getObjectTypeOrClassStringObjectType());
653: }
654:
655: public function isVoid(): TrinaryLogic
656: {
657: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isVoid());
658: }
659:
660: public function isScalar(): TrinaryLogic
661: {
662: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isScalar());
663: }
664:
665: public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType
666: {
667: return new BooleanType();
668: }
669:
670: public function isOffsetAccessible(): TrinaryLogic
671: {
672: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessible());
673: }
674:
675: public function isOffsetAccessLegal(): TrinaryLogic
676: {
677: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isOffsetAccessLegal());
678: }
679:
680: public function hasOffsetValueType(Type $offsetType): TrinaryLogic
681: {
682: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
683: $arrayKeyOffsetType = $offsetType->toArrayKey();
684: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
685: return TrinaryLogic::createYes();
686: }
687: }
688:
689: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
690: }
691:
692: public function getOffsetValueType(Type $offsetType): Type
693: {
694: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
695: if ($this->isOversizedArray()->yes()) {
696: return TypeUtils::toBenevolentUnion($result);
697: }
698:
699: return $result;
700: }
701:
702: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
703: {
704: return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
705: }
706:
707: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
708: {
709: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
710: }
711:
712: public function unsetOffset(Type $offsetType): Type
713: {
714: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
715: }
716:
717: public function getKeysArray(): Type
718: {
719: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
720: }
721:
722: public function getValuesArray(): Type
723: {
724: return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray());
725: }
726:
727: public function fillKeysArray(Type $valueType): Type
728: {
729: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
730: }
731:
732: public function flipArray(): Type
733: {
734: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
735: }
736:
737: public function intersectKeyArray(Type $otherArraysType): Type
738: {
739: return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
740: }
741:
742: public function popArray(): Type
743: {
744: return $this->intersectTypes(static fn (Type $type): Type => $type->popArray());
745: }
746:
747: public function searchArray(Type $needleType): Type
748: {
749: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType));
750: }
751:
752: public function shiftArray(): Type
753: {
754: return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray());
755: }
756:
757: public function shuffleArray(): Type
758: {
759: return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
760: }
761:
762: public function getEnumCases(): array
763: {
764: $compare = [];
765: foreach ($this->types as $type) {
766: $oneType = [];
767: foreach ($type->getEnumCases() as $enumCase) {
768: $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase;
769: }
770: $compare[] = $oneType;
771: }
772:
773: return array_values(array_intersect_key(...$compare));
774: }
775:
776: public function isCallable(): TrinaryLogic
777: {
778: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
779: }
780:
781: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
782: {
783: if ($this->isCallable()->no()) {
784: throw new ShouldNotHappenException();
785: }
786:
787: return [new TrivialParametersAcceptor()];
788: }
789:
790: public function isCloneable(): TrinaryLogic
791: {
792: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
793: }
794:
795: public function isSmallerThan(Type $otherType): TrinaryLogic
796: {
797: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType));
798: }
799:
800: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
801: {
802: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType));
803: }
804:
805: public function isNull(): TrinaryLogic
806: {
807: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
808: }
809:
810: public function isConstantValue(): TrinaryLogic
811: {
812: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
813: }
814:
815: public function isConstantScalarValue(): TrinaryLogic
816: {
817: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
818: }
819:
820: public function getConstantScalarTypes(): array
821: {
822: $scalarTypes = [];
823: foreach ($this->types as $type) {
824: foreach ($type->getConstantScalarTypes() as $scalarType) {
825: $scalarTypes[] = $scalarType;
826: }
827: }
828:
829: return $scalarTypes;
830: }
831:
832: public function getConstantScalarValues(): array
833: {
834: $values = [];
835: foreach ($this->types as $type) {
836: foreach ($type->getConstantScalarValues() as $value) {
837: $values[] = $value;
838: }
839: }
840:
841: return $values;
842: }
843:
844: public function isTrue(): TrinaryLogic
845: {
846: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
847: }
848:
849: public function isFalse(): TrinaryLogic
850: {
851: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
852: }
853:
854: public function isBoolean(): TrinaryLogic
855: {
856: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
857: }
858:
859: public function isFloat(): TrinaryLogic
860: {
861: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
862: }
863:
864: public function isInteger(): TrinaryLogic
865: {
866: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
867: }
868:
869: public function isGreaterThan(Type $otherType): TrinaryLogic
870: {
871: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type));
872: }
873:
874: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
875: {
876: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type));
877: }
878:
879: public function getSmallerType(): Type
880: {
881: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType());
882: }
883:
884: public function getSmallerOrEqualType(): Type
885: {
886: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType());
887: }
888:
889: public function getGreaterType(): Type
890: {
891: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType());
892: }
893:
894: public function getGreaterOrEqualType(): Type
895: {
896: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType());
897: }
898:
899: public function toBoolean(): BooleanType
900: {
901: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
902:
903: if (!$type instanceof BooleanType) {
904: return new BooleanType();
905: }
906:
907: return $type;
908: }
909:
910: public function toNumber(): Type
911: {
912: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
913:
914: return $type;
915: }
916:
917: public function toString(): Type
918: {
919: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
920:
921: return $type;
922: }
923:
924: public function toInteger(): Type
925: {
926: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
927:
928: return $type;
929: }
930:
931: public function toFloat(): Type
932: {
933: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
934:
935: return $type;
936: }
937:
938: public function toArray(): Type
939: {
940: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
941:
942: return $type;
943: }
944:
945: public function toArrayKey(): Type
946: {
947: if ($this->isNumericString()->yes()) {
948: return new IntegerType();
949: }
950:
951: if ($this->isString()->yes()) {
952: return $this;
953: }
954:
955: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
956: }
957:
958: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
959: {
960: $types = TemplateTypeMap::createEmpty();
961:
962: foreach ($this->types as $type) {
963: $types = $types->intersect($type->inferTemplateTypes($receivedType));
964: }
965:
966: return $types;
967: }
968:
969: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
970: {
971: $references = [];
972:
973: foreach ($this->types as $type) {
974: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
975: $references[] = $reference;
976: }
977: }
978:
979: return $references;
980: }
981:
982: public function traverse(callable $cb): Type
983: {
984: $types = [];
985: $changed = false;
986:
987: foreach ($this->types as $type) {
988: $newType = $cb($type);
989: if ($type !== $newType) {
990: $changed = true;
991: }
992: $types[] = $newType;
993: }
994:
995: if ($changed) {
996: return TypeCombinator::intersect(...$types);
997: }
998:
999: return $this;
1000: }
1001:
1002: public function traverseSimultaneously(Type $right, callable $cb): Type
1003: {
1004: $types = [];
1005: $changed = false;
1006:
1007: if (!$right instanceof self) {
1008: return $this;
1009: }
1010:
1011: if (count($this->getTypes()) !== count($right->getTypes())) {
1012: return $this;
1013: }
1014:
1015: foreach ($this->getSortedTypes() as $i => $leftType) {
1016: $rightType = $right->getSortedTypes()[$i];
1017: $newType = $cb($leftType, $rightType);
1018: if ($leftType !== $newType) {
1019: $changed = true;
1020: }
1021: $types[] = $newType;
1022: }
1023:
1024: if ($changed) {
1025: return TypeCombinator::intersect(...$types);
1026: }
1027:
1028: return $this;
1029: }
1030:
1031: public function tryRemove(Type $typeToRemove): ?Type
1032: {
1033: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1034: }
1035:
1036: public function exponentiate(Type $exponent): Type
1037: {
1038: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1039: }
1040:
1041: public function getFiniteTypes(): array
1042: {
1043: $compare = [];
1044: foreach ($this->types as $type) {
1045: $oneType = [];
1046: foreach ($type->getFiniteTypes() as $finiteType) {
1047: $oneType[md5($finiteType->describe(VerbosityLevel::typeOnly()))] = $finiteType;
1048: }
1049: $compare[] = $oneType;
1050: }
1051:
1052: $result = array_values(array_intersect_key(...$compare));
1053:
1054: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1055: return [];
1056: }
1057:
1058: return $result;
1059: }
1060:
1061: /**
1062: * @param mixed[] $properties
1063: */
1064: public static function __set_state(array $properties): Type
1065: {
1066: return new self($properties['types']);
1067: }
1068:
1069: /**
1070: * @param callable(Type $type): TrinaryLogic $getResult
1071: */
1072: private function intersectResults(callable $getResult): TrinaryLogic
1073: {
1074: return TrinaryLogic::lazyMaxMin($this->types, $getResult);
1075: }
1076:
1077: /**
1078: * @param callable(Type $type): Type $getType
1079: */
1080: private function intersectTypes(callable $getType): Type
1081: {
1082: $operands = array_map($getType, $this->types);
1083: return TypeCombinator::intersect(...$operands);
1084: }
1085:
1086: public function toPhpDocNode(): TypeNode
1087: {
1088: $baseTypes = [];
1089: $typesToDescribe = [];
1090: $skipTypeNames = [];
1091:
1092: $nonEmptyStr = false;
1093: $nonFalsyStr = false;
1094:
1095: foreach ($this->getSortedTypes() as $i => $type) {
1096: if ($type instanceof AccessoryNonEmptyStringType
1097: || $type instanceof AccessoryLiteralStringType
1098: || $type instanceof AccessoryNumericStringType
1099: || $type instanceof AccessoryNonFalsyStringType
1100: ) {
1101: if ($type instanceof AccessoryNonFalsyStringType) {
1102: $nonFalsyStr = true;
1103: }
1104: if ($type instanceof AccessoryNonEmptyStringType) {
1105: $nonEmptyStr = true;
1106: }
1107: if ($nonEmptyStr && $nonFalsyStr) {
1108: // prevent redundant 'non-empty-string&non-falsy-string'
1109: foreach ($typesToDescribe as $key => $typeToDescribe) {
1110: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1111: continue;
1112: }
1113:
1114: unset($typesToDescribe[$key]);
1115: }
1116: }
1117:
1118: $typesToDescribe[$i] = $type;
1119: $skipTypeNames[] = 'string';
1120: continue;
1121: }
1122: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1123: $typesToDescribe[$i] = $type;
1124: $skipTypeNames[] = 'array';
1125: continue;
1126: }
1127: if (!$type instanceof AccessoryType) {
1128: $baseTypes[$i] = $type;
1129: continue;
1130: }
1131:
1132: $accessoryPhpDocNode = $type->toPhpDocNode();
1133: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1134: continue;
1135: }
1136:
1137: $typesToDescribe[$i] = $type;
1138: }
1139:
1140: $describedTypes = [];
1141: foreach ($baseTypes as $i => $type) {
1142: $typeNode = $type->toPhpDocNode();
1143: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1144: $nonEmpty = false;
1145: $typeName = 'array';
1146: foreach ($typesToDescribe as $j => $typeToDescribe) {
1147: if ($typeToDescribe instanceof AccessoryArrayListType) {
1148: $typeName = 'list';
1149: if (count($typeNode->genericTypes) > 1) {
1150: array_shift($typeNode->genericTypes);
1151: }
1152: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1153: $nonEmpty = true;
1154: } else {
1155: continue;
1156: }
1157:
1158: unset($typesToDescribe[$j]);
1159: }
1160:
1161: if ($nonEmpty) {
1162: $typeName = 'non-empty-' . $typeName;
1163: }
1164:
1165: $describedTypes[$i] = new GenericTypeNode(
1166: new IdentifierTypeNode($typeName),
1167: $typeNode->genericTypes,
1168: );
1169: continue;
1170: }
1171:
1172: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1173: continue;
1174: }
1175:
1176: $describedTypes[$i] = $typeNode;
1177: }
1178:
1179: foreach ($typesToDescribe as $i => $typeToDescribe) {
1180: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1181: }
1182:
1183: ksort($describedTypes);
1184:
1185: $describedTypes = array_values($describedTypes);
1186:
1187: if (count($describedTypes) === 1) {
1188: return $describedTypes[0];
1189: }
1190:
1191: return new IntersectionTypeNode($describedTypes);
1192: }
1193:
1194: }
1195: