1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Type;
4:
5: use PHPStan\TrinaryLogic;
6: use PHPStan\Type\Accessory\AccessoryArrayListType;
7: use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType;
8: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
9: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
10: use PHPStan\Type\Accessory\AccessoryType;
11: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
12: use PHPStan\Type\Accessory\HasOffsetType;
13: use PHPStan\Type\Accessory\HasOffsetValueType;
14: use PHPStan\Type\Accessory\HasPropertyType;
15: use PHPStan\Type\Accessory\NonEmptyArrayType;
16: use PHPStan\Type\Accessory\OversizedArrayType;
17: use PHPStan\Type\Constant\ConstantArrayType;
18: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
19: use PHPStan\Type\Constant\ConstantBooleanType;
20: use PHPStan\Type\Constant\ConstantFloatType;
21: use PHPStan\Type\Constant\ConstantIntegerType;
22: use PHPStan\Type\Constant\ConstantStringType;
23: use PHPStan\Type\Generic\GenericClassStringType;
24: use PHPStan\Type\Generic\TemplateArrayType;
25: use PHPStan\Type\Generic\TemplateBenevolentUnionType;
26: use PHPStan\Type\Generic\TemplateMixedType;
27: use PHPStan\Type\Generic\TemplateType;
28: use PHPStan\Type\Generic\TemplateTypeFactory;
29: use PHPStan\Type\Generic\TemplateUnionType;
30: use function array_fill;
31: use function array_filter;
32: use function array_key_exists;
33: use function array_key_first;
34: use function array_merge;
35: use function array_slice;
36: use function array_splice;
37: use function array_values;
38: use function count;
39: use function get_class;
40: use function in_array;
41: use function is_int;
42: use function sprintf;
43: use function usort;
44: use const PHP_INT_MAX;
45: use const PHP_INT_MIN;
46:
47: /**
48: * @api
49: */
50: final class TypeCombinator
51: {
52:
53: public static function addNull(Type $type): Type
54: {
55: $nullType = new NullType();
56:
57: if ($nullType->isSuperTypeOf($type)->no()) {
58: return self::union($type, $nullType);
59: }
60:
61: return $type;
62: }
63:
64: public static function remove(Type $fromType, Type $typeToRemove): Type
65: {
66: if ($typeToRemove instanceof UnionType) {
67: foreach ($typeToRemove->getTypes() as $unionTypeToRemove) {
68: $fromType = self::remove($fromType, $unionTypeToRemove);
69: }
70: return $fromType;
71: }
72:
73: $isSuperType = $typeToRemove->isSuperTypeOf($fromType);
74: if ($isSuperType->yes()) {
75: return new NeverType();
76: }
77: if ($isSuperType->no()) {
78: return $fromType;
79: }
80:
81: if ($typeToRemove instanceof MixedType) {
82: $typeToRemoveSubtractedType = $typeToRemove->getSubtractedType();
83: if ($typeToRemoveSubtractedType !== null) {
84: return self::intersect($fromType, $typeToRemoveSubtractedType);
85: }
86: }
87:
88: $removed = $fromType->tryRemove($typeToRemove);
89: if ($removed !== null) {
90: return $removed;
91: }
92:
93: $fromFiniteTypes = $fromType->getFiniteTypes();
94: if (count($fromFiniteTypes) > 0) {
95: $finiteTypesToRemove = $typeToRemove->getFiniteTypes();
96: if (count($finiteTypesToRemove) > 0) {
97: $result = [];
98: foreach ($fromFiniteTypes as $finiteType) {
99: foreach ($finiteTypesToRemove as $finiteTypeToRemove) {
100: if ($finiteType->equals($finiteTypeToRemove)) {
101: continue 2;
102: }
103: }
104:
105: $result[] = $finiteType;
106: }
107:
108: if (count($result) === count($fromFiniteTypes)) {
109: return $fromType;
110: }
111:
112: if (count($result) === 0) {
113: return new NeverType();
114: }
115:
116: if (count($result) === 1) {
117: return $result[0];
118: }
119:
120: return new UnionType($result);
121: }
122: }
123:
124: return $fromType;
125: }
126:
127: public static function removeNull(Type $type): Type
128: {
129: if (self::containsNull($type)) {
130: return self::remove($type, new NullType());
131: }
132:
133: return $type;
134: }
135:
136: public static function containsNull(Type $type): bool
137: {
138: if ($type instanceof UnionType) {
139: foreach ($type->getTypes() as $innerType) {
140: if ($innerType instanceof NullType) {
141: return true;
142: }
143: }
144:
145: return false;
146: }
147:
148: return $type instanceof NullType;
149: }
150:
151: public static function union(Type ...$types): Type
152: {
153: $typesCount = count($types);
154: if ($typesCount === 0) {
155: return new NeverType();
156: }
157:
158: // Fast path for single non-union type
159: if ($typesCount === 1) {
160: $singleType = $types[0];
161: if (!$singleType instanceof UnionType && !$singleType->isArray()->yes()) {
162: return $singleType;
163: }
164: }
165:
166: // Fast path for common 2-type cases
167: if ($typesCount === 2) {
168: $a = $types[0];
169: $b = $types[1];
170:
171: // union(never, X) = X and union(X, never) = X
172: if ($a instanceof NeverType && !$a->isExplicit()) {
173: return $b;
174: }
175: if ($b instanceof NeverType && !$b->isExplicit()) {
176: return $a;
177: }
178:
179: // union(mixed, X) = mixed (non-explicit, non-template, no subtracted)
180: if ($a instanceof MixedType && !$a->isExplicitMixed() && !$a instanceof TemplateMixedType && $a->getSubtractedType() === null) {
181: return $a;
182: }
183: if ($b instanceof MixedType && !$b->isExplicitMixed() && !$b instanceof TemplateMixedType && $b->getSubtractedType() === null) {
184: return $b;
185: }
186:
187: // union(X, X) = X (same object identity)
188: if ($a === $b) {
189: return $a;
190: }
191: }
192:
193: $alreadyNormalized = [];
194: $alreadyNormalizedCounter = 0;
195:
196: $benevolentTypes = [];
197: $benevolentUnionObject = null;
198: $neverCount = 0;
199: // transform A | (B | C) to A | B | C
200: for ($i = 0; $i < $typesCount; $i++) {
201: if (
202: $types[$i] instanceof MixedType
203: && !$types[$i]->isExplicitMixed()
204: && !$types[$i] instanceof TemplateMixedType
205: && $types[$i]->getSubtractedType() === null
206: ) {
207: return $types[$i];
208: }
209: if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
210: $neverCount++;
211: continue;
212: }
213: if ($types[$i] instanceof BenevolentUnionType) {
214: if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) {
215: $benevolentUnionObject = $types[$i];
216: }
217: $benevolentTypesCount = 0;
218: $typesInner = $types[$i]->getTypes();
219: foreach ($typesInner as $benevolentInnerType) {
220: $benevolentTypesCount++;
221: $benevolentTypes[$benevolentInnerType->describe(VerbosityLevel::value())] = $benevolentInnerType;
222: }
223: array_splice($types, $i, 1, $typesInner);
224: $typesCount += $benevolentTypesCount - 1;
225: continue;
226: }
227: if (!($types[$i] instanceof UnionType)) {
228: continue;
229: }
230: if ($types[$i] instanceof TemplateType) {
231: continue;
232: }
233:
234: $typesInner = $types[$i]->getTypes();
235: $alreadyNormalized[$alreadyNormalizedCounter] = $typesInner;
236: $alreadyNormalizedCounter++;
237: array_splice($types, $i, 1, $typesInner);
238: $typesCount += count($typesInner) - 1;
239: }
240:
241: // Bulk-remove implicit NeverTypes (skipped during the loop above)
242: if ($neverCount > 0) {
243: if ($neverCount === $typesCount) {
244: return new NeverType();
245: }
246:
247: $filtered = [];
248: for ($i = 0; $i < $typesCount; $i++) {
249: if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
250: continue;
251: }
252: $filtered[] = $types[$i];
253: }
254: $types = $filtered;
255: $typesCount = count($types);
256:
257: if ($typesCount === 0) {
258: return new NeverType();
259: }
260: if ($typesCount === 1 && !$types[0]->isArray()->yes()) {
261: return $types[0];
262: }
263: if ($typesCount === 2) {
264: return self::union($types[0], $types[1]);
265: }
266: }
267:
268: if ($typesCount === 0) {
269: return new NeverType();
270: }
271:
272: if ($typesCount === 1 && !$types[0]->isArray()->yes()) {
273: return $types[0];
274: }
275:
276: $arrayTypes = [];
277: $scalarTypes = [];
278: $hasGenericScalarTypes = [];
279: $enumCaseTypes = [];
280: $integerRangeTypes = [];
281: for ($i = 0; $i < $typesCount; $i++) {
282: if ($types[$i]->isConstantScalarValue()->yes()) {
283: $type = $types[$i];
284: $scalarTypes[get_class($type)][$type->describe(VerbosityLevel::cache())] = $type;
285: unset($types[$i]);
286: continue;
287: }
288:
289: if ($types[$i]->isBoolean()->yes()) {
290: $hasGenericScalarTypes[ConstantBooleanType::class] = true;
291: } elseif ($types[$i]->isFloat()->yes()) {
292: $hasGenericScalarTypes[ConstantFloatType::class] = true;
293: } elseif ($types[$i]->isInteger()->yes() && !$types[$i] instanceof IntegerRangeType) {
294: $hasGenericScalarTypes[ConstantIntegerType::class] = true;
295: } elseif ($types[$i]->isString()->yes() && $types[$i]->isClassString()->no() && TypeUtils::getAccessoryTypes($types[$i]) === []) {
296: $hasGenericScalarTypes[ConstantStringType::class] = true;
297: } else {
298: $enumCase = $types[$i]->getEnumCaseObject();
299: if ($enumCase !== null) {
300: $enumCaseTypes[$types[$i]->describe(VerbosityLevel::cache())] = $types[$i];
301:
302: unset($types[$i]);
303: continue;
304: }
305: }
306:
307: if ($types[$i] instanceof IntegerRangeType) {
308: $integerRangeTypes[] = $types[$i];
309: unset($types[$i]);
310:
311: continue;
312: }
313:
314: if (!$types[$i]->isArray()->yes()) {
315: continue;
316: }
317:
318: $arrayTypes[] = $types[$i];
319: unset($types[$i]);
320: }
321:
322: $enumCaseTypes = array_values($enumCaseTypes);
323: usort(
324: $integerRangeTypes,
325: static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN)
326: ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX),
327: );
328: $types = array_merge($types, $integerRangeTypes);
329: $types = array_values($types);
330: $typesCount = count($types);
331:
332: foreach ($scalarTypes as $classType => $scalarTypeItems) {
333: if (isset($hasGenericScalarTypes[$classType])) {
334: unset($scalarTypes[$classType]);
335: continue;
336: }
337: if ($classType === ConstantBooleanType::class && count($scalarTypeItems) === 2) {
338: $types[] = new BooleanType();
339: $typesCount++;
340: unset($scalarTypes[$classType]);
341: continue;
342: }
343:
344: $scalarTypeItems = array_values($scalarTypeItems);
345: $scalarTypeItemsCount = count($scalarTypeItems);
346: for ($i = 0; $i < $typesCount; $i++) {
347: for ($j = 0; $j < $scalarTypeItemsCount; $j++) {
348: $compareResult = self::compareTypesInUnion($types[$i], $scalarTypeItems[$j]);
349: if ($compareResult === null) {
350: continue;
351: }
352:
353: [$a, $b] = $compareResult;
354: if ($a !== null) {
355: $types[$i] = $a;
356: array_splice($scalarTypeItems, $j--, 1);
357: $scalarTypeItemsCount--;
358: continue 1;
359: }
360: if ($b !== null) {
361: $scalarTypeItems[$j] = $b;
362: array_splice($types, $i--, 1);
363: $typesCount--;
364: continue 2;
365: }
366: }
367: }
368:
369: $scalarTypes[$classType] = $scalarTypeItems;
370: }
371:
372: if (count($types) > 16) {
373: $newTypes = [];
374: foreach ($types as $type) {
375: $newTypes[$type->describe(VerbosityLevel::cache())] = $type;
376: }
377: $types = array_values($newTypes);
378: }
379:
380: $types = array_merge(
381: $types,
382: self::processArrayTypes($arrayTypes),
383: );
384: $typesCount = count($types);
385:
386: // transform A | A to A
387: // transform A | never to A
388: for ($i = 0; $i < $typesCount; $i++) {
389: for ($j = $i + 1; $j < $typesCount; $j++) {
390: if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) {
391: continue;
392: }
393: $compareResult = self::compareTypesInUnion($types[$i], $types[$j]);
394: if ($compareResult === null) {
395: continue;
396: }
397:
398: [$a, $b] = $compareResult;
399: if ($a !== null) {
400: $types[$i] = $a;
401: array_splice($types, $j--, 1);
402: $typesCount--;
403: continue 1;
404: }
405: if ($b !== null) {
406: $types[$j] = $b;
407: array_splice($types, $i--, 1);
408: $typesCount--;
409: continue 2;
410: }
411: }
412: }
413:
414: $enumCasesCount = count($enumCaseTypes);
415: for ($i = 0; $i < $typesCount; $i++) {
416: for ($j = 0; $j < $enumCasesCount; $j++) {
417: $compareResult = self::compareTypesInUnion($types[$i], $enumCaseTypes[$j]);
418: if ($compareResult === null) {
419: continue;
420: }
421:
422: [$a, $b] = $compareResult;
423: if ($a !== null) {
424: $types[$i] = $a;
425: array_splice($enumCaseTypes, $j--, 1);
426: $enumCasesCount--;
427: continue 1;
428: }
429: if ($b !== null) {
430: $enumCaseTypes[$j] = $b;
431: array_splice($types, $i--, 1);
432: $typesCount--;
433: continue 2;
434: }
435: }
436: }
437:
438: foreach ($enumCaseTypes as $enumCaseType) {
439: $types[] = $enumCaseType;
440: $typesCount++;
441: }
442:
443: foreach ($scalarTypes as $scalarTypeItems) {
444: foreach ($scalarTypeItems as $scalarType) {
445: $types[] = $scalarType;
446: $typesCount++;
447: }
448: }
449:
450: if ($typesCount === 0) {
451: return new NeverType();
452: }
453: if ($typesCount === 1) {
454: return $types[0];
455: }
456:
457: if ($benevolentTypes !== []) {
458: $tempTypes = $types;
459: foreach ($tempTypes as $i => $type) {
460: if (!isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) {
461: break;
462: }
463:
464: unset($tempTypes[$i]);
465: }
466:
467: if ($tempTypes === []) {
468: if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) {
469: return $benevolentUnionObject->withTypes(array_values($types));
470: }
471:
472: return new BenevolentUnionType(array_values($types), true);
473: }
474: }
475:
476: return new UnionType(array_values($types), true);
477: }
478:
479: /**
480: * @param array<int, Type[]> $alreadyNormalized
481: */
482: private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool
483: {
484: foreach ($alreadyNormalized as $normalizedTypes) {
485: foreach ($normalizedTypes as $i => $normalizedType) {
486: if ($normalizedType !== $a) {
487: continue;
488: }
489:
490: foreach ($normalizedTypes as $j => $anotherNormalizedType) {
491: if ($i === $j) {
492: continue;
493: }
494: if ($anotherNormalizedType === $b) {
495: return true;
496: }
497: }
498: }
499: }
500:
501: return false;
502: }
503:
504: /**
505: * @return array{Type, null}|array{null, Type}|null
506: */
507: private static function compareTypesInUnion(Type $a, Type $b): ?array
508: {
509: if ($a instanceof IntegerRangeType) {
510: $type = $a->tryUnion($b);
511: if ($type !== null) {
512: $a = $type;
513: return [$a, null];
514: }
515: }
516: if ($b instanceof IntegerRangeType) {
517: $type = $b->tryUnion($a);
518: if ($type !== null) {
519: $b = $type;
520: return [null, $b];
521: }
522: }
523: if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) {
524: return null;
525: }
526: if ($a instanceof HasOffsetValueType && $b instanceof HasOffsetValueType) {
527: if ($a->getOffsetType()->equals($b->getOffsetType())) {
528: return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null];
529: }
530: }
531: if ($a instanceof IntersectionType && $b instanceof IntersectionType) {
532: $merged = self::mergeIntersectionsForUnion($a, $b);
533: if ($merged !== null) {
534: return [$merged, null];
535: }
536: }
537: if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) {
538: return null;
539: }
540:
541: // simplify string[] | int[] to (string|int)[]
542: if ($a instanceof IterableType && $b instanceof IterableType) {
543: return [
544: new IterableType(
545: self::union($a->getIterableKeyType(), $b->getIterableKeyType()),
546: self::union($a->getIterableValueType(), $b->getIterableValueType()),
547: ),
548: null,
549: ];
550: }
551:
552: if ($a instanceof SubtractableType) {
553: $typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType();
554: if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) {
555: $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($b);
556: } else {
557: $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b);
558: }
559: if ($isSuperType->yes()) {
560: $a = self::intersectWithSubtractedType($a, $b);
561: return [$a, null];
562: }
563: }
564:
565: if ($b instanceof SubtractableType) {
566: $typeWithoutSubtractedTypeB = $b->getTypeWithoutSubtractedType();
567: if ($typeWithoutSubtractedTypeB instanceof MixedType && $a instanceof MixedType) {
568: $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($a);
569: } else {
570: $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a);
571: }
572: if ($isSuperType->yes()) {
573: $b = self::intersectWithSubtractedType($b, $a);
574: return [null, $b];
575: }
576: }
577:
578: if ($b->isSuperTypeOf($a)->yes()) {
579: return [null, $b];
580: }
581:
582: if ($a->isSuperTypeOf($b)->yes()) {
583: return [$a, null];
584: }
585:
586: if (
587: $a instanceof ConstantStringType
588: ) {
589: if ($a->getValue() === '') {
590: $description = $b->describe(VerbosityLevel::value());
591: if (in_array($description, ['non-empty-string', 'non-falsy-string'], true)) {
592: return [null, self::intersect(
593: new StringType(),
594: ...self::getAccessoryCaseStringTypes($b),
595: )];
596: }
597: }
598:
599: if ($a->getValue() === '0') {
600: $description = $b->describe(VerbosityLevel::value());
601: if ($description === 'non-falsy-string') {
602: return [null, new IntersectionType([
603: new StringType(),
604: new AccessoryNonEmptyStringType(),
605: ...self::getAccessoryCaseStringTypes($b),
606: ])];
607: }
608: }
609: }
610:
611: if (
612: $b instanceof ConstantStringType
613: ) {
614: if ($b->getValue() === '') {
615: $description = $a->describe(VerbosityLevel::value());
616: if (in_array($description, ['non-empty-string', 'non-falsy-string'], true)) {
617: return [self::intersect(
618: new StringType(),
619: ...self::getAccessoryCaseStringTypes($a),
620: ), null];
621: }
622: }
623:
624: if ($b->getValue() === '0') {
625: $description = $a->describe(VerbosityLevel::value());
626: if ($description === 'non-falsy-string') {
627: return [new IntersectionType([
628: new StringType(),
629: new AccessoryNonEmptyStringType(),
630: ...self::getAccessoryCaseStringTypes($a),
631: ]), null];
632: }
633: }
634: }
635:
636: // numeric-string | non-decimal-int-string → string (preserving common accessories)
637: // Works because decimal-int-string ⊂ numeric-string, so together they cover all strings
638: if ($a->isString()->yes() && $b->isString()->yes()) {
639: $decimalIntString = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]);
640: if ($b->isDecimalIntegerString()->no()) {
641: $bBase = self::removeDecimalIntStringAccessory($b);
642: if ($bBase->isSuperTypeOf($a)->yes() && $a->isSuperTypeOf($decimalIntString)->yes()) {
643: return [null, $bBase];
644: }
645: }
646: if ($a->isDecimalIntegerString()->no()) {
647: $aBase = self::removeDecimalIntStringAccessory($a);
648: if ($aBase->isSuperTypeOf($b)->yes() && $b->isSuperTypeOf($decimalIntString)->yes()) {
649: return [$aBase, null];
650: }
651: }
652: }
653:
654: return null;
655: }
656:
657: /**
658: * @return list<Type>
659: */
660: private static function getAccessoryCaseStringTypes(Type $type): array
661: {
662: $accessory = [];
663: if ($type->isLowercaseString()->yes()) {
664: $accessory[] = new AccessoryLowercaseStringType();
665: }
666: if ($type->isUppercaseString()->yes()) {
667: $accessory[] = new AccessoryUppercaseStringType();
668: }
669:
670: return $accessory;
671: }
672:
673: private static function removeDecimalIntStringAccessory(Type $type): Type
674: {
675: if (!$type instanceof IntersectionType) {
676: return $type;
677: }
678:
679: return self::intersect(...array_filter(
680: $type->getTypes(),
681: static fn (Type $t): bool => !$t instanceof AccessoryDecimalIntegerStringType,
682: ));
683: }
684:
685: private static function unionWithSubtractedType(
686: Type $type,
687: ?Type $subtractedType,
688: ): Type
689: {
690: if ($subtractedType === null) {
691: return $type;
692: }
693:
694: if ($subtractedType instanceof SubtractableType) {
695: $withoutSubtracted = $subtractedType->getTypeWithoutSubtractedType();
696: if ($withoutSubtracted->isSuperTypeOf($type)->yes()) {
697: $subtractedSubtractedType = $subtractedType->getSubtractedType();
698: if ($subtractedSubtractedType === null) {
699: return new NeverType();
700: }
701:
702: return self::intersect($type, $subtractedSubtractedType);
703: }
704: }
705:
706: if ($type instanceof SubtractableType) {
707: $subtractedType = $type->getSubtractedType() === null
708: ? $subtractedType
709: : self::union($type->getSubtractedType(), $subtractedType);
710:
711: $subtractedType = self::intersect(
712: $type->getTypeWithoutSubtractedType(),
713: $subtractedType,
714: );
715: if ($subtractedType instanceof NeverType) {
716: $subtractedType = null;
717: }
718:
719: return $type->changeSubtractedType($subtractedType);
720: }
721:
722: if ($subtractedType->isSuperTypeOf($type)->yes()) {
723: return new NeverType();
724: }
725:
726: return self::remove($type, $subtractedType);
727: }
728:
729: private static function intersectWithSubtractedType(
730: SubtractableType $a,
731: Type $b,
732: ): Type
733: {
734: if ($a->getSubtractedType() === null || $b instanceof NeverType) {
735: return $a;
736: }
737:
738: if ($b instanceof IntersectionType) {
739: $subtractableTypes = [];
740: foreach ($b->getTypes() as $innerType) {
741: if (!$innerType instanceof SubtractableType) {
742: continue;
743: }
744:
745: $subtractableTypes[] = $innerType;
746: }
747:
748: if (count($subtractableTypes) === 0) {
749: return $a->getTypeWithoutSubtractedType();
750: }
751:
752: $subtractedTypes = [];
753: foreach ($subtractableTypes as $subtractableType) {
754: if ($subtractableType->getSubtractedType() === null) {
755: continue;
756: }
757:
758: $subtractedTypes[] = $subtractableType->getSubtractedType();
759: }
760:
761: if (count($subtractedTypes) === 0) {
762: return $a->getTypeWithoutSubtractedType();
763:
764: }
765:
766: $subtractedType = self::union(...$subtractedTypes);
767: } else {
768: $isBAlreadySubtracted = $a->getSubtractedType()->isSuperTypeOf($b);
769:
770: if ($isBAlreadySubtracted->no()) {
771: return $a;
772: } elseif ($isBAlreadySubtracted->yes()) {
773: $subtractedType = self::remove($a->getSubtractedType(), $b);
774:
775: if (
776: $subtractedType instanceof NeverType
777: || !$subtractedType->isSuperTypeOf($b)->no()
778: ) {
779: $subtractedType = null;
780: }
781:
782: return $a->changeSubtractedType($subtractedType);
783: } elseif ($b instanceof SubtractableType) {
784: $subtractedType = $b->getSubtractedType();
785: if ($subtractedType === null) {
786: return $a->getTypeWithoutSubtractedType();
787: }
788: } else {
789: $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
790: if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
791: return $a->getTypeWithoutSubtractedType();
792: }
793: $subtractedType = new MixedType(subtractedType: $b);
794: }
795: }
796:
797: $subtractedType = self::intersect(
798: $a->getSubtractedType(),
799: $subtractedType,
800: );
801: if ($subtractedType instanceof NeverType) {
802: $subtractedType = null;
803: }
804:
805: return $a->changeSubtractedType($subtractedType);
806: }
807:
808: /**
809: * @param Type[] $arrayTypes
810: * @return list<Type>
811: */
812: private static function processArrayAccessoryTypes(array $arrayTypes): array
813: {
814: $isIterableAtLeastOnce = [];
815: $accessoryTypes = [];
816: foreach ($arrayTypes as $i => $arrayType) {
817: $isIterableAtLeastOnce[] = $arrayType->isIterableAtLeastOnce();
818:
819: if ($arrayType instanceof IntersectionType) {
820: foreach ($arrayType->getTypes() as $innerType) {
821: if ($innerType instanceof TemplateType) {
822: break;
823: }
824: if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) {
825: continue;
826: }
827: if ($innerType instanceof HasOffsetType) {
828: $innerType = new HasOffsetValueType($innerType->getOffsetType(), $arrayType->getIterableValueType());
829: }
830: if ($innerType instanceof HasOffsetValueType) {
831: $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType;
832: continue;
833: }
834:
835: $accessoryTypes[$innerType->describe(VerbosityLevel::cache())][$i] = $innerType;
836: }
837: }
838:
839: if (!$arrayType->isConstantArray()->yes()) {
840: continue;
841: }
842: $constantArrays = $arrayType->getConstantArrays();
843:
844: foreach ($constantArrays as $constantArray) {
845: if ($constantArray->isList()->yes()) {
846: $list = new AccessoryArrayListType();
847: $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list;
848: }
849:
850: if (!$constantArray->isIterableAtLeastOnce()->yes()) {
851: continue;
852: }
853:
854: $nonEmpty = new NonEmptyArrayType();
855: $accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty;
856: }
857: }
858:
859: $commonAccessoryTypes = [];
860: $arrayTypeCount = count($arrayTypes);
861: foreach ($accessoryTypes as $accessoryType) {
862: if (count($accessoryType) !== $arrayTypeCount) {
863: $firstKey = array_key_first($accessoryType);
864: if ($accessoryType[$firstKey] instanceof OversizedArrayType) {
865: $commonAccessoryTypes[] = $accessoryType[$firstKey];
866: }
867: continue;
868: }
869:
870: if ($accessoryType[0] instanceof HasOffsetValueType) {
871: $commonAccessoryTypes[] = self::union(...$accessoryType);
872: continue;
873: }
874:
875: $commonAccessoryTypes[] = $accessoryType[0];
876: }
877:
878: if (TrinaryLogic::createYes()->and(...$isIterableAtLeastOnce)->yes()) {
879: $commonAccessoryTypes[] = new NonEmptyArrayType();
880: }
881:
882: return $commonAccessoryTypes;
883: }
884:
885: /**
886: * @param list<Type> $arrayTypes
887: * @return Type[]
888: */
889: private static function processArrayTypes(array $arrayTypes): array
890: {
891: if ($arrayTypes === []) {
892: return [];
893: }
894:
895: $accessoryTypes = self::processArrayAccessoryTypes($arrayTypes);
896:
897: if (count($arrayTypes) === 1) {
898: return [
899: self::intersect(...$arrayTypes, ...$accessoryTypes),
900: ];
901: }
902:
903: $keyTypesForGeneralArray = [];
904: $valueTypesForGeneralArray = [];
905: $generalArrayOccurred = false;
906: $constantKeyTypesNumbered = [];
907: $filledArrays = 0;
908: $overflowed = false;
909:
910: /** @var int|float $nextConstantKeyTypeIndex */
911: $nextConstantKeyTypeIndex = 1;
912:
913: foreach ($arrayTypes as $arrayType) {
914: $constantArrays = $arrayType->getConstantArrays();
915: $isConstantArray = $constantArrays !== [];
916: if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) {
917: $filledArrays++;
918: }
919:
920: if ($generalArrayOccurred || !$isConstantArray) {
921: foreach ($arrayType->getArrays() as $type) {
922: $keyTypesForGeneralArray[] = $type->getIterableKeyType();
923: $valueTypesForGeneralArray[] = $type->getItemType();
924: $generalArrayOccurred = true;
925: }
926: continue;
927: }
928:
929: foreach ($constantArrays as $constantArray) {
930: $valueTypes = $constantArray->getValueTypes();
931: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
932: $valueTypesForGeneralArray[] = $valueTypes[$i];
933:
934: $keyTypeValue = $keyType->getValue();
935: if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) {
936: continue;
937: }
938: $keyTypesForGeneralArray[] = $keyType;
939:
940: $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex;
941: $nextConstantKeyTypeIndex *= 2;
942: if (!is_int($nextConstantKeyTypeIndex)) {
943: $generalArrayOccurred = true;
944: $overflowed = true;
945: continue 2;
946: }
947: }
948: }
949: }
950:
951: if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) {
952: $reducedArrayTypes = self::reduceArrays($arrayTypes, false);
953: if (count($reducedArrayTypes) === 1) {
954: return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)];
955: }
956:
957: $templateArrayType = null;
958: foreach ($arrayTypes as $arrayType) {
959: if (!$arrayType instanceof TemplateArrayType) {
960: $templateArrayType = null;
961: break;
962: }
963:
964: if ($templateArrayType !== null) {
965: continue;
966: }
967:
968: $templateArrayType = $arrayType;
969: }
970:
971: $arrayType = new ArrayType(
972: self::union(...$keyTypesForGeneralArray),
973: self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
974: );
975:
976: if ($templateArrayType !== null) {
977: $arrayType = new TemplateArrayType(
978: $templateArrayType->getScope(),
979: $templateArrayType->getStrategy(),
980: $templateArrayType->getVariance(),
981: $templateArrayType->getName(),
982: $arrayType,
983: $templateArrayType->getDefault(),
984: );
985: }
986:
987: return [
988: self::intersect($arrayType, ...$accessoryTypes),
989: ];
990: }
991:
992: $reducedArrayTypes = self::optimizeConstantArrays(self::reduceArrays($arrayTypes, true));
993: foreach ($reducedArrayTypes as $idx => $reducedArray) {
994: $reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$accessoryTypes);
995: }
996: return $reducedArrayTypes;
997: }
998:
999: /**
1000: * @param Type[] $types
1001: * @return Type[]
1002: */
1003: private static function optimizeConstantArrays(array $types): array
1004: {
1005: $constantArrayValuesCount = self::countConstantArrayValueTypes($types);
1006:
1007: if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1008: return $types;
1009: }
1010:
1011: $results = [];
1012: $eachIsOversized = true;
1013: foreach ($types as $type) {
1014: $isOversized = false;
1015: $result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type {
1016: if (!$type instanceof ConstantArrayType) {
1017: return $traverse($type);
1018: }
1019:
1020: if ($type->isIterableAtLeastOnce()->no()) {
1021: return $type;
1022: }
1023:
1024: $isOversized = true;
1025:
1026: $isList = true;
1027: $valueTypes = [];
1028: $keyTypes = [];
1029: $nextAutoIndex = 0;
1030: $innerValueTypes = $type->getValueTypes();
1031: foreach ($type->getKeyTypes() as $i => $innerKeyType) {
1032: if (!$innerKeyType instanceof ConstantIntegerType) {
1033: $isList = false;
1034: } elseif ($innerKeyType->getValue() !== $nextAutoIndex) {
1035: $isList = false;
1036: $nextAutoIndex = $innerKeyType->getValue() + 1;
1037: } else {
1038: $nextAutoIndex++;
1039: }
1040:
1041: $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific());
1042: $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;
1043:
1044: $generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type) use ($traverse): Type {
1045: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
1046: return new IntersectionType([$type, new OversizedArrayType()]);
1047: }
1048:
1049: if ($type instanceof ConstantScalarType) {
1050: return $type->generalize(GeneralizePrecision::moreSpecific());
1051: }
1052:
1053: return $traverse($type);
1054: });
1055: $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
1056: }
1057:
1058: $keyType = TypeCombinator::union(...array_values($keyTypes));
1059: $valueType = TypeCombinator::union(...array_values($valueTypes));
1060:
1061: $accessories = [];
1062: if ($isList) {
1063: $accessories[] = new AccessoryArrayListType();
1064: }
1065: $accessories[] = new NonEmptyArrayType();
1066: $accessories[] = new OversizedArrayType();
1067:
1068: return self::intersect(new ArrayType($keyType, $valueType), ...$accessories);
1069: });
1070:
1071: if (!$isOversized) {
1072: $eachIsOversized = false;
1073: }
1074:
1075: $results[] = $result;
1076: }
1077:
1078: if ($eachIsOversized) {
1079: $eachIsList = true;
1080: $keyTypes = [];
1081: $valueTypes = [];
1082: foreach ($results as $result) {
1083: $keyTypes[] = $result->getIterableKeyType();
1084: $valueTypes[] = $result->getIterableValueType();
1085: if ($result->isList()->yes()) {
1086: continue;
1087: }
1088: $eachIsList = false;
1089: }
1090:
1091: $keyType = self::union(...$keyTypes);
1092: $valueType = self::union(...$valueTypes);
1093:
1094: if ($valueType instanceof UnionType && count($valueType->getTypes()) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1095: $valueType = $valueType->generalize(GeneralizePrecision::lessSpecific());
1096: }
1097:
1098: $accessories = [];
1099: if ($eachIsList) {
1100: $accessories[] = new AccessoryArrayListType();
1101: }
1102: $accessories[] = new NonEmptyArrayType();
1103: $accessories[] = new OversizedArrayType();
1104:
1105: return [self::intersect(new ArrayType($keyType, $valueType), ...$accessories)];
1106: }
1107:
1108: return $results;
1109: }
1110:
1111: /**
1112: * @param Type[] $types
1113: */
1114: public static function countConstantArrayValueTypes(array $types): int
1115: {
1116: $constantArrayValuesCount = 0;
1117: foreach ($types as $type) {
1118: TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type {
1119: if ($type instanceof ConstantArrayType) {
1120: $constantArrayValuesCount += count($type->getValueTypes());
1121: }
1122:
1123: return $traverse($type);
1124: });
1125: }
1126: return $constantArrayValuesCount;
1127: }
1128:
1129: /**
1130: * @param list<Type> $constantArrays
1131: * @return list<Type>
1132: */
1133: private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array
1134: {
1135: $newArrays = [];
1136: $arraysToProcess = [];
1137: $emptyArray = null;
1138: foreach ($constantArrays as $constantArray) {
1139: if (!$constantArray->isConstantArray()->yes()) {
1140: // This is an optimization for current use-case of $preserveTaggedUnions=false, where we need
1141: // one constant array as a result, or we generalize the $constantArrays.
1142: if (!$preserveTaggedUnions) {
1143: return $constantArrays;
1144: }
1145: $newArrays[] = $constantArray;
1146: continue;
1147: }
1148:
1149: if ($constantArray->isIterableAtLeastOnce()->no()) {
1150: $emptyArray = $constantArray;
1151: continue;
1152: }
1153:
1154: $arraysToProcess = array_merge($arraysToProcess, $constantArray->getConstantArrays());
1155: }
1156:
1157: if ($emptyArray !== null) {
1158: $newArrays[] = $emptyArray;
1159: }
1160:
1161: $arraysToProcessPerKey = [];
1162: foreach ($arraysToProcess as $i => $arrayToProcess) {
1163: foreach ($arrayToProcess->getKeyTypes() as $keyType) {
1164: $arraysToProcessPerKey[$keyType->getValue()][] = $i;
1165: }
1166: }
1167:
1168: $eligibleCombinations = [];
1169:
1170: foreach ($arraysToProcessPerKey as $arrays) {
1171: for ($i = 0, $arraysCount = count($arrays); $i < $arraysCount - 1; $i++) {
1172: for ($j = $i + 1; $j < $arraysCount; $j++) {
1173: $eligibleCombinations[$arrays[$i]][$arrays[$j]] ??= 0;
1174: $eligibleCombinations[$arrays[$i]][$arrays[$j]]++;
1175: }
1176: }
1177: }
1178:
1179: foreach ($eligibleCombinations as $i => $other) {
1180: if (!array_key_exists($i, $arraysToProcess)) {
1181: continue;
1182: }
1183:
1184: foreach ($other as $j => $overlappingKeysCount) {
1185: if (!array_key_exists($j, $arraysToProcess)) {
1186: continue;
1187: }
1188:
1189: if (
1190: $preserveTaggedUnions
1191: && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1192: && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])
1193: ) {
1194: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1195: unset($arraysToProcess[$i]);
1196: continue 2;
1197: }
1198:
1199: if (
1200: $preserveTaggedUnions
1201: && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1202: && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])
1203: ) {
1204: $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1205: unset($arraysToProcess[$j]);
1206: continue 1;
1207: }
1208:
1209: if (
1210: !$preserveTaggedUnions
1211: // both arrays have same keys
1212: && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1213: && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1214: ) {
1215: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1216: unset($arraysToProcess[$i]);
1217: continue 2;
1218: }
1219: }
1220: }
1221:
1222: return array_merge($newArrays, $arraysToProcess);
1223: }
1224:
1225: public static function intersect(Type ...$types): Type
1226: {
1227: $typesCount = count($types);
1228: if ($typesCount === 0) {
1229: return new NeverType();
1230: }
1231:
1232: $types = array_values($types);
1233: if ($typesCount === 1) {
1234: return $types[0];
1235: }
1236:
1237: foreach ($types as $type) {
1238: if ($type instanceof NeverType && !$type->isExplicit()) {
1239: return $type;
1240: }
1241: }
1242:
1243: $sortTypes = static function (Type $a, Type $b): int {
1244: if (!$a instanceof UnionType || !$b instanceof UnionType) {
1245: return 0;
1246: }
1247:
1248: if ($a instanceof TemplateType) {
1249: return -1;
1250: }
1251: if ($b instanceof TemplateType) {
1252: return 1;
1253: }
1254:
1255: if ($a instanceof BenevolentUnionType) {
1256: return -1;
1257: }
1258: if ($b instanceof BenevolentUnionType) {
1259: return 1;
1260: }
1261:
1262: return 0;
1263: };
1264: usort($types, $sortTypes);
1265: // transform A & (B | C) to (A & B) | (A & C)
1266: foreach ($types as $i => $type) {
1267: if (!$type instanceof UnionType) {
1268: continue;
1269: }
1270:
1271: $topLevelUnionSubTypes = [];
1272: $innerTypes = $type->getTypes();
1273: usort($innerTypes, $sortTypes);
1274: $slice1 = array_slice($types, 0, $i);
1275: $slice2 = array_slice($types, $i + 1);
1276: foreach ($innerTypes as $innerUnionSubType) {
1277: $topLevelUnionSubTypes[] = self::intersect(
1278: $innerUnionSubType,
1279: ...$slice1,
1280: ...$slice2,
1281: );
1282: }
1283:
1284: $union = self::union(...$topLevelUnionSubTypes);
1285: if ($union instanceof NeverType) {
1286: return $union;
1287: }
1288:
1289: if ($type instanceof BenevolentUnionType) {
1290: $union = TypeUtils::toBenevolentUnion($union);
1291: }
1292:
1293: if ($type instanceof TemplateUnionType || $type instanceof TemplateBenevolentUnionType) {
1294: $union = TemplateTypeFactory::create(
1295: $type->getScope(),
1296: $type->getName(),
1297: $union,
1298: $type->getVariance(),
1299: $type->getStrategy(),
1300: $type->getDefault(),
1301: );
1302: }
1303:
1304: return $union;
1305: }
1306:
1307: $newTypes = [];
1308: $hasOffsetValueTypeCount = 0;
1309: $typesCount = count($types);
1310: for ($i = 0; $i < $typesCount; $i++) {
1311: $type = $types[$i];
1312:
1313: if ($type instanceof IntersectionType && !$type instanceof TemplateType) {
1314: // transform A & (B & C) to A & B & C
1315: array_splice($types, $i--, 1, $type->getTypes());
1316: $typesCount = count($types);
1317: } elseif ($type instanceof HasOffsetValueType) {
1318: $hasOffsetValueTypeCount++;
1319: } else {
1320: $newTypes[] = $type;
1321: }
1322: }
1323:
1324: if ($hasOffsetValueTypeCount > 32) {
1325: $newTypes[] = new OversizedArrayType();
1326: $types = $newTypes;
1327: $typesCount = count($types);
1328: }
1329:
1330: usort($types, static function (Type $a, Type $b): int {
1331: // move subtractables with subtracts before those without to avoid losing them in the union logic
1332: if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) {
1333: return -1;
1334: }
1335: if ($b instanceof SubtractableType && $b->getSubtractedType() !== null) {
1336: return 1;
1337: }
1338:
1339: if ($a instanceof ConstantArrayType && !$b instanceof ConstantArrayType) {
1340: return -1;
1341: }
1342: if ($b instanceof ConstantArrayType && !$a instanceof ConstantArrayType) {
1343: return 1;
1344: }
1345:
1346: return 0;
1347: });
1348:
1349: // transform IntegerType & ConstantIntegerType to ConstantIntegerType
1350: // transform Child & Parent to Child
1351: // transform Object & ~null to Object
1352: // transform A & A to A
1353: // transform int[] & string to never
1354: // transform callable & int to never
1355: // transform A & ~A to never
1356: // transform int & string to never
1357: for ($i = 0; $i < $typesCount; $i++) {
1358: for ($j = $i + 1; $j < $typesCount; $j++) {
1359: if ($types[$j] instanceof SubtractableType) {
1360: $typeWithoutSubtractedTypeA = $types[$j]->getTypeWithoutSubtractedType();
1361:
1362: if ($typeWithoutSubtractedTypeA instanceof MixedType && $types[$i] instanceof MixedType) {
1363: $isSuperTypeSubtractableA = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($types[$i]);
1364: } else {
1365: $isSuperTypeSubtractableA = $typeWithoutSubtractedTypeA->isSuperTypeOf($types[$i]);
1366: }
1367: if ($isSuperTypeSubtractableA->yes()) {
1368: $types[$i] = self::unionWithSubtractedType($types[$i], $types[$j]->getSubtractedType());
1369: array_splice($types, $j--, 1);
1370: $typesCount--;
1371: continue 1;
1372: }
1373: }
1374:
1375: if ($types[$i] instanceof SubtractableType) {
1376: $typeWithoutSubtractedTypeB = $types[$i]->getTypeWithoutSubtractedType();
1377:
1378: if ($typeWithoutSubtractedTypeB instanceof MixedType && $types[$j] instanceof MixedType) {
1379: $isSuperTypeSubtractableB = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($types[$j]);
1380: } else {
1381: $isSuperTypeSubtractableB = $typeWithoutSubtractedTypeB->isSuperTypeOf($types[$j]);
1382: }
1383: if ($isSuperTypeSubtractableB->yes()) {
1384: $types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType());
1385: array_splice($types, $i--, 1);
1386: $typesCount--;
1387: continue 2;
1388: }
1389: }
1390:
1391: if ($types[$i] instanceof IntegerRangeType) {
1392: $intersectionType = $types[$i]->tryIntersect($types[$j]);
1393: if ($intersectionType !== null) {
1394: $types[$j] = $intersectionType;
1395: array_splice($types, $i--, 1);
1396: $typesCount--;
1397: continue 2;
1398: }
1399: }
1400:
1401: if ($types[$j] instanceof IterableType) {
1402: $isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]);
1403: } else {
1404: $isSuperTypeA = $types[$j]->isSuperTypeOf($types[$i]);
1405: }
1406:
1407: if ($isSuperTypeA->yes()) {
1408: array_splice($types, $j--, 1);
1409: $typesCount--;
1410: continue;
1411: }
1412:
1413: if ($types[$i] instanceof IterableType) {
1414: $isSuperTypeB = $types[$i]->isSuperTypeOfMixed($types[$j]);
1415: } else {
1416: $isSuperTypeB = $types[$i]->isSuperTypeOf($types[$j]);
1417: }
1418:
1419: if ($isSuperTypeB->maybe()) {
1420: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetType) {
1421: $types[$i] = $types[$i]->makeOffsetRequired($types[$j]->getOffsetType());
1422: array_splice($types, $j--, 1);
1423: $typesCount--;
1424: continue;
1425: }
1426:
1427: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetType) {
1428: $types[$j] = $types[$j]->makeOffsetRequired($types[$i]->getOffsetType());
1429: array_splice($types, $i--, 1);
1430: $typesCount--;
1431: continue 2;
1432: }
1433:
1434: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof AccessoryArrayListType) {
1435: $types[$i] = $types[$i]->makeList();
1436: array_splice($types, $j--, 1);
1437: $typesCount--;
1438: continue;
1439: }
1440:
1441: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof AccessoryArrayListType) {
1442: $types[$j] = $types[$j]->makeList();
1443: array_splice($types, $i--, 1);
1444: $typesCount--;
1445: continue 2;
1446: }
1447:
1448: if (
1449: $types[$i] instanceof ConstantArrayType
1450: && $types[$j] instanceof NonEmptyArrayType
1451: && (count($types[$i]->getKeyTypes()) === 1 || $types[$i]->isList()->yes())
1452: && $types[$i]->isOptionalKey(0)
1453: ) {
1454: $types[$i] = $types[$i]->makeOffsetRequired($types[$i]->getKeyTypes()[0]);
1455: array_splice($types, $j--, 1);
1456: $typesCount--;
1457: continue;
1458: }
1459:
1460: if (
1461: $types[$j] instanceof ConstantArrayType
1462: && $types[$i] instanceof NonEmptyArrayType
1463: && (count($types[$j]->getKeyTypes()) === 1 || $types[$j]->isList()->yes())
1464: && $types[$j]->isOptionalKey(0)
1465: ) {
1466: $types[$j] = $types[$j]->makeOffsetRequired($types[$j]->getKeyTypes()[0]);
1467: array_splice($types, $i--, 1);
1468: $typesCount--;
1469: continue 2;
1470: }
1471:
1472: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetValueType) {
1473: $offsetType = $types[$j]->getOffsetType();
1474: $valueType = $types[$j]->getValueType();
1475: $newValueType = self::intersect($types[$i]->getOffsetValueType($offsetType), $valueType);
1476: if ($newValueType instanceof NeverType) {
1477: return $newValueType;
1478: }
1479: $types[$i] = $types[$i]->setOffsetValueType($offsetType, $newValueType);
1480: array_splice($types, $j--, 1);
1481: $typesCount--;
1482: continue;
1483: }
1484:
1485: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetValueType) {
1486: $offsetType = $types[$i]->getOffsetType();
1487: $valueType = $types[$i]->getValueType();
1488: $newValueType = self::intersect($types[$j]->getOffsetValueType($offsetType), $valueType);
1489: if ($newValueType instanceof NeverType) {
1490: return $newValueType;
1491: }
1492:
1493: $types[$j] = $types[$j]->setOffsetValueType($offsetType, $newValueType);
1494: array_splice($types, $i--, 1);
1495: $typesCount--;
1496: continue 2;
1497: }
1498:
1499: if ($types[$i] instanceof OversizedArrayType && $types[$j] instanceof HasOffsetValueType) {
1500: array_splice($types, $j--, 1);
1501: $typesCount--;
1502: continue;
1503: }
1504:
1505: if ($types[$j] instanceof OversizedArrayType && $types[$i] instanceof HasOffsetValueType) {
1506: array_splice($types, $i--, 1);
1507: $typesCount--;
1508: continue 2;
1509: }
1510:
1511: if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
1512: $types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName());
1513: array_splice($types, $j--, 1);
1514: $typesCount--;
1515: continue;
1516: }
1517:
1518: if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
1519: $types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName());
1520: array_splice($types, $i--, 1);
1521: $typesCount--;
1522: continue 2;
1523: }
1524:
1525: if ($types[$i] instanceof ConstantArrayType && ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType)) {
1526: $newArray = ConstantArrayTypeBuilder::createEmpty();
1527: $valueTypes = $types[$i]->getValueTypes();
1528: foreach ($types[$i]->getKeyTypes() as $k => $keyType) {
1529: $newArray->setOffsetValueType(
1530: self::intersect($keyType, $types[$j]->getIterableKeyType()),
1531: self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()),
1532: $types[$i]->isOptionalKey($k) && !$types[$j]->hasOffsetValueType($keyType)->yes(),
1533: );
1534: }
1535: $types[$i] = $newArray->getArray();
1536: array_splice($types, $j--, 1);
1537: $typesCount--;
1538: continue 2;
1539: }
1540:
1541: if ($types[$j] instanceof ConstantArrayType && ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType)) {
1542: $newArray = ConstantArrayTypeBuilder::createEmpty();
1543: $valueTypes = $types[$j]->getValueTypes();
1544: foreach ($types[$j]->getKeyTypes() as $k => $keyType) {
1545: $newArray->setOffsetValueType(
1546: self::intersect($keyType, $types[$i]->getIterableKeyType()),
1547: self::intersect($valueTypes[$k], $types[$i]->getIterableValueType()),
1548: $types[$j]->isOptionalKey($k) && !$types[$i]->hasOffsetValueType($keyType)->yes(),
1549: );
1550: }
1551: $types[$j] = $newArray->getArray();
1552: array_splice($types, $i--, 1);
1553: $typesCount--;
1554: continue 2;
1555: }
1556:
1557: if (
1558: ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType || $types[$i] instanceof IterableType) &&
1559: ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType || $types[$j] instanceof IterableType)
1560: ) {
1561: $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType());
1562: $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType());
1563: if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) {
1564: $types[$j] = new IterableType($keyType, $itemType);
1565: } else {
1566: $types[$j] = new ArrayType($keyType, $itemType);
1567: }
1568: array_splice($types, $i--, 1);
1569: $typesCount--;
1570: continue 2;
1571: }
1572:
1573: if ($types[$i] instanceof GenericClassStringType && $types[$j] instanceof GenericClassStringType) {
1574: $genericType = self::intersect($types[$i]->getGenericType(), $types[$j]->getGenericType());
1575: $types[$i] = new GenericClassStringType($genericType);
1576: array_splice($types, $j--, 1);
1577: $typesCount--;
1578: continue;
1579: }
1580:
1581: if (
1582: $types[$i] instanceof ArrayType
1583: && get_class($types[$i]) === ArrayType::class
1584: && $types[$j] instanceof AccessoryArrayListType
1585: && !$types[$j]->getIterableKeyType()->isSuperTypeOf($types[$i]->getIterableKeyType())->yes()
1586: ) {
1587: $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType());
1588: if ($keyType instanceof NeverType) {
1589: return $keyType;
1590: }
1591: $types[$i] = new ArrayType($keyType, $types[$i]->getItemType());
1592: continue;
1593: }
1594:
1595: continue;
1596: }
1597:
1598: if ($isSuperTypeB->yes()) {
1599: array_splice($types, $i--, 1);
1600: $typesCount--;
1601: continue 2;
1602: }
1603:
1604: if ($isSuperTypeA->no()) {
1605: return new NeverType();
1606: }
1607: }
1608: }
1609:
1610: if ($typesCount === 1) {
1611: return $types[0];
1612: }
1613:
1614: return new IntersectionType($types);
1615: }
1616:
1617: /**
1618: * Merge two IntersectionTypes that have the same structure but differ
1619: * in HasOffsetValueType value types (matched by offset key).
1620: *
1621: * E.g. (A & hasOV('k', X)) | (A & hasOV('k', Y)) → (A & hasOV('k', X|Y))
1622: */
1623: private static function mergeIntersectionsForUnion(IntersectionType $a, IntersectionType $b): ?Type
1624: {
1625: $aTypes = $a->getTypes();
1626: $bTypes = $b->getTypes();
1627:
1628: if (count($aTypes) !== count($bTypes)) {
1629: return null;
1630: }
1631:
1632: $mergedTypes = [];
1633: $hasDifference = false;
1634: $bUsed = array_fill(0, count($bTypes), false);
1635:
1636: foreach ($aTypes as $aType) {
1637: $matched = false;
1638: foreach ($bTypes as $bIdx => $bType) {
1639: if ($bUsed[$bIdx]) {
1640: continue;
1641: }
1642:
1643: if ($aType->equals($bType)) {
1644: $mergedTypes[] = $aType;
1645: $bUsed[$bIdx] = true;
1646: $matched = true;
1647: break;
1648: }
1649:
1650: // HasOffsetValueType: merge value types when offset keys match
1651: if ($aType instanceof HasOffsetValueType && $bType instanceof HasOffsetValueType
1652: && $aType->getOffsetType()->equals($bType->getOffsetType())) {
1653: $mergedTypes[] = new HasOffsetValueType(
1654: $aType->getOffsetType(),
1655: self::union($aType->getValueType(), $bType->getValueType()),
1656: );
1657: $hasDifference = true;
1658: $bUsed[$bIdx] = true;
1659: $matched = true;
1660: break;
1661: }
1662:
1663: // HasOffsetType, HasMethodType, HasPropertyType: only equal values match (no merging possible)
1664: }
1665: if (!$matched) {
1666: return null;
1667: }
1668: }
1669:
1670: if (!$hasDifference) {
1671: return null;
1672: }
1673:
1674: return self::intersect(...$mergedTypes);
1675: }
1676:
1677: public static function removeFalsey(Type $type): Type
1678: {
1679: return self::remove($type, StaticTypeFactory::falsey());
1680: }
1681:
1682: public static function removeTruthy(Type $type): Type
1683: {
1684: return self::remove($type, StaticTypeFactory::truthy());
1685: }
1686:
1687: }
1688: