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 hasOffsetValueType(Type $offsetType): TrinaryLogic
676: {
677: if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
678: $arrayKeyOffsetType = $offsetType->toArrayKey();
679: if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
680: return TrinaryLogic::createYes();
681: }
682: }
683:
684: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->hasOffsetValueType($offsetType));
685: }
686:
687: public function getOffsetValueType(Type $offsetType): Type
688: {
689: $result = $this->intersectTypes(static fn (Type $type): Type => $type->getOffsetValueType($offsetType));
690: if ($this->isOversizedArray()->yes()) {
691: return TypeUtils::toBenevolentUnion($result);
692: }
693:
694: return $result;
695: }
696:
697: public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $unionValues = true): Type
698: {
699: return $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
700: }
701:
702: public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
703: {
704: return $this->intersectTypes(static fn (Type $type): Type => $type->setExistingOffsetValueType($offsetType, $valueType));
705: }
706:
707: public function unsetOffset(Type $offsetType): Type
708: {
709: return $this->intersectTypes(static fn (Type $type): Type => $type->unsetOffset($offsetType));
710: }
711:
712: public function getKeysArray(): Type
713: {
714: return $this->intersectTypes(static fn (Type $type): Type => $type->getKeysArray());
715: }
716:
717: public function getValuesArray(): Type
718: {
719: return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray());
720: }
721:
722: public function fillKeysArray(Type $valueType): Type
723: {
724: return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));
725: }
726:
727: public function flipArray(): Type
728: {
729: return $this->intersectTypes(static fn (Type $type): Type => $type->flipArray());
730: }
731:
732: public function intersectKeyArray(Type $otherArraysType): Type
733: {
734: return $this->intersectTypes(static fn (Type $type): Type => $type->intersectKeyArray($otherArraysType));
735: }
736:
737: public function popArray(): Type
738: {
739: return $this->intersectTypes(static fn (Type $type): Type => $type->popArray());
740: }
741:
742: public function searchArray(Type $needleType): Type
743: {
744: return $this->intersectTypes(static fn (Type $type): Type => $type->searchArray($needleType));
745: }
746:
747: public function shiftArray(): Type
748: {
749: return $this->intersectTypes(static fn (Type $type): Type => $type->shiftArray());
750: }
751:
752: public function shuffleArray(): Type
753: {
754: return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
755: }
756:
757: public function getEnumCases(): array
758: {
759: $compare = [];
760: foreach ($this->types as $type) {
761: $oneType = [];
762: foreach ($type->getEnumCases() as $enumCase) {
763: $oneType[md5($enumCase->describe(VerbosityLevel::typeOnly()))] = $enumCase;
764: }
765: $compare[] = $oneType;
766: }
767:
768: return array_values(array_intersect_key(...$compare));
769: }
770:
771: public function isCallable(): TrinaryLogic
772: {
773: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCallable());
774: }
775:
776: public function getCallableParametersAcceptors(ClassMemberAccessAnswerer $scope): array
777: {
778: if ($this->isCallable()->no()) {
779: throw new ShouldNotHappenException();
780: }
781:
782: return [new TrivialParametersAcceptor()];
783: }
784:
785: public function isCloneable(): TrinaryLogic
786: {
787: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isCloneable());
788: }
789:
790: public function isSmallerThan(Type $otherType): TrinaryLogic
791: {
792: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThan($otherType));
793: }
794:
795: public function isSmallerThanOrEqual(Type $otherType): TrinaryLogic
796: {
797: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isSmallerThanOrEqual($otherType));
798: }
799:
800: public function isNull(): TrinaryLogic
801: {
802: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isNull());
803: }
804:
805: public function isConstantValue(): TrinaryLogic
806: {
807: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantValue());
808: }
809:
810: public function isConstantScalarValue(): TrinaryLogic
811: {
812: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isConstantScalarValue());
813: }
814:
815: public function getConstantScalarTypes(): array
816: {
817: $scalarTypes = [];
818: foreach ($this->types as $type) {
819: foreach ($type->getConstantScalarTypes() as $scalarType) {
820: $scalarTypes[] = $scalarType;
821: }
822: }
823:
824: return $scalarTypes;
825: }
826:
827: public function getConstantScalarValues(): array
828: {
829: $values = [];
830: foreach ($this->types as $type) {
831: foreach ($type->getConstantScalarValues() as $value) {
832: $values[] = $value;
833: }
834: }
835:
836: return $values;
837: }
838:
839: public function isTrue(): TrinaryLogic
840: {
841: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isTrue());
842: }
843:
844: public function isFalse(): TrinaryLogic
845: {
846: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFalse());
847: }
848:
849: public function isBoolean(): TrinaryLogic
850: {
851: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isBoolean());
852: }
853:
854: public function isFloat(): TrinaryLogic
855: {
856: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isFloat());
857: }
858:
859: public function isInteger(): TrinaryLogic
860: {
861: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->isInteger());
862: }
863:
864: public function isGreaterThan(Type $otherType): TrinaryLogic
865: {
866: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThan($type));
867: }
868:
869: public function isGreaterThanOrEqual(Type $otherType): TrinaryLogic
870: {
871: return $this->intersectResults(static fn (Type $type): TrinaryLogic => $otherType->isSmallerThanOrEqual($type));
872: }
873:
874: public function getSmallerType(): Type
875: {
876: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerType());
877: }
878:
879: public function getSmallerOrEqualType(): Type
880: {
881: return $this->intersectTypes(static fn (Type $type): Type => $type->getSmallerOrEqualType());
882: }
883:
884: public function getGreaterType(): Type
885: {
886: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterType());
887: }
888:
889: public function getGreaterOrEqualType(): Type
890: {
891: return $this->intersectTypes(static fn (Type $type): Type => $type->getGreaterOrEqualType());
892: }
893:
894: public function toBoolean(): BooleanType
895: {
896: $type = $this->intersectTypes(static fn (Type $type): BooleanType => $type->toBoolean());
897:
898: if (!$type instanceof BooleanType) {
899: return new BooleanType();
900: }
901:
902: return $type;
903: }
904:
905: public function toNumber(): Type
906: {
907: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toNumber());
908:
909: return $type;
910: }
911:
912: public function toString(): Type
913: {
914: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString());
915:
916: return $type;
917: }
918:
919: public function toInteger(): Type
920: {
921: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toInteger());
922:
923: return $type;
924: }
925:
926: public function toFloat(): Type
927: {
928: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toFloat());
929:
930: return $type;
931: }
932:
933: public function toArray(): Type
934: {
935: $type = $this->intersectTypes(static fn (Type $type): Type => $type->toArray());
936:
937: return $type;
938: }
939:
940: public function toArrayKey(): Type
941: {
942: if ($this->isNumericString()->yes()) {
943: return new IntegerType();
944: }
945:
946: if ($this->isString()->yes()) {
947: return $this;
948: }
949:
950: return $this->intersectTypes(static fn (Type $type): Type => $type->toArrayKey());
951: }
952:
953: public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
954: {
955: $types = TemplateTypeMap::createEmpty();
956:
957: foreach ($this->types as $type) {
958: $types = $types->intersect($type->inferTemplateTypes($receivedType));
959: }
960:
961: return $types;
962: }
963:
964: public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
965: {
966: $references = [];
967:
968: foreach ($this->types as $type) {
969: foreach ($type->getReferencedTemplateTypes($positionVariance) as $reference) {
970: $references[] = $reference;
971: }
972: }
973:
974: return $references;
975: }
976:
977: public function traverse(callable $cb): Type
978: {
979: $types = [];
980: $changed = false;
981:
982: foreach ($this->types as $type) {
983: $newType = $cb($type);
984: if ($type !== $newType) {
985: $changed = true;
986: }
987: $types[] = $newType;
988: }
989:
990: if ($changed) {
991: return TypeCombinator::intersect(...$types);
992: }
993:
994: return $this;
995: }
996:
997: public function traverseSimultaneously(Type $right, callable $cb): Type
998: {
999: $types = [];
1000: $changed = false;
1001:
1002: if (!$right instanceof self) {
1003: return $this;
1004: }
1005:
1006: if (count($this->getTypes()) !== count($right->getTypes())) {
1007: return $this;
1008: }
1009:
1010: foreach ($this->getSortedTypes() as $i => $leftType) {
1011: $rightType = $right->getSortedTypes()[$i];
1012: $newType = $cb($leftType, $rightType);
1013: if ($leftType !== $newType) {
1014: $changed = true;
1015: }
1016: $types[] = $newType;
1017: }
1018:
1019: if ($changed) {
1020: return TypeCombinator::intersect(...$types);
1021: }
1022:
1023: return $this;
1024: }
1025:
1026: public function tryRemove(Type $typeToRemove): ?Type
1027: {
1028: return $this->intersectTypes(static fn (Type $type): Type => TypeCombinator::remove($type, $typeToRemove));
1029: }
1030:
1031: public function exponentiate(Type $exponent): Type
1032: {
1033: return $this->intersectTypes(static fn (Type $type): Type => $type->exponentiate($exponent));
1034: }
1035:
1036: public function getFiniteTypes(): array
1037: {
1038: $compare = [];
1039: foreach ($this->types as $type) {
1040: $oneType = [];
1041: foreach ($type->getFiniteTypes() as $finiteType) {
1042: $oneType[md5($finiteType->describe(VerbosityLevel::typeOnly()))] = $finiteType;
1043: }
1044: $compare[] = $oneType;
1045: }
1046:
1047: $result = array_values(array_intersect_key(...$compare));
1048:
1049: if (count($result) > InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT) {
1050: return [];
1051: }
1052:
1053: return $result;
1054: }
1055:
1056: /**
1057: * @param mixed[] $properties
1058: */
1059: public static function __set_state(array $properties): Type
1060: {
1061: return new self($properties['types']);
1062: }
1063:
1064: /**
1065: * @param callable(Type $type): TrinaryLogic $getResult
1066: */
1067: private function intersectResults(callable $getResult): TrinaryLogic
1068: {
1069: return TrinaryLogic::lazyMaxMin($this->types, $getResult);
1070: }
1071:
1072: /**
1073: * @param callable(Type $type): Type $getType
1074: */
1075: private function intersectTypes(callable $getType): Type
1076: {
1077: $operands = array_map($getType, $this->types);
1078: return TypeCombinator::intersect(...$operands);
1079: }
1080:
1081: public function toPhpDocNode(): TypeNode
1082: {
1083: $baseTypes = [];
1084: $typesToDescribe = [];
1085: $skipTypeNames = [];
1086:
1087: $nonEmptyStr = false;
1088: $nonFalsyStr = false;
1089:
1090: foreach ($this->getSortedTypes() as $i => $type) {
1091: if ($type instanceof AccessoryNonEmptyStringType
1092: || $type instanceof AccessoryLiteralStringType
1093: || $type instanceof AccessoryNumericStringType
1094: || $type instanceof AccessoryNonFalsyStringType
1095: ) {
1096: if ($type instanceof AccessoryNonFalsyStringType) {
1097: $nonFalsyStr = true;
1098: }
1099: if ($type instanceof AccessoryNonEmptyStringType) {
1100: $nonEmptyStr = true;
1101: }
1102: if ($nonEmptyStr && $nonFalsyStr) {
1103: // prevent redundant 'non-empty-string&non-falsy-string'
1104: foreach ($typesToDescribe as $key => $typeToDescribe) {
1105: if (!($typeToDescribe instanceof AccessoryNonEmptyStringType)) {
1106: continue;
1107: }
1108:
1109: unset($typesToDescribe[$key]);
1110: }
1111: }
1112:
1113: $typesToDescribe[$i] = $type;
1114: $skipTypeNames[] = 'string';
1115: continue;
1116: }
1117: if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
1118: $typesToDescribe[$i] = $type;
1119: $skipTypeNames[] = 'array';
1120: continue;
1121: }
1122: if (!$type instanceof AccessoryType) {
1123: $baseTypes[$i] = $type;
1124: continue;
1125: }
1126:
1127: $accessoryPhpDocNode = $type->toPhpDocNode();
1128: if ($accessoryPhpDocNode instanceof IdentifierTypeNode && $accessoryPhpDocNode->name === '') {
1129: continue;
1130: }
1131:
1132: $typesToDescribe[$i] = $type;
1133: }
1134:
1135: $describedTypes = [];
1136: foreach ($baseTypes as $i => $type) {
1137: $typeNode = $type->toPhpDocNode();
1138: if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
1139: $nonEmpty = false;
1140: $typeName = 'array';
1141: foreach ($typesToDescribe as $j => $typeToDescribe) {
1142: if ($typeToDescribe instanceof AccessoryArrayListType) {
1143: $typeName = 'list';
1144: if (count($typeNode->genericTypes) > 1) {
1145: array_shift($typeNode->genericTypes);
1146: }
1147: } elseif ($typeToDescribe instanceof NonEmptyArrayType) {
1148: $nonEmpty = true;
1149: } else {
1150: continue;
1151: }
1152:
1153: unset($typesToDescribe[$j]);
1154: }
1155:
1156: if ($nonEmpty) {
1157: $typeName = 'non-empty-' . $typeName;
1158: }
1159:
1160: $describedTypes[$i] = new GenericTypeNode(
1161: new IdentifierTypeNode($typeName),
1162: $typeNode->genericTypes,
1163: );
1164: continue;
1165: }
1166:
1167: if ($typeNode instanceof IdentifierTypeNode && in_array($typeNode->name, $skipTypeNames, true)) {
1168: continue;
1169: }
1170:
1171: $describedTypes[$i] = $typeNode;
1172: }
1173:
1174: foreach ($typesToDescribe as $i => $typeToDescribe) {
1175: $describedTypes[$i] = $typeToDescribe->toPhpDocNode();
1176: }
1177:
1178: ksort($describedTypes);
1179:
1180: $describedTypes = array_values($describedTypes);
1181:
1182: if (count($describedTypes) === 1) {
1183: return $describedTypes[0];
1184: }
1185:
1186: return new IntersectionTypeNode($describedTypes);
1187: }
1188:
1189: }
1190: