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_keys;
35: use function array_merge;
36: use function array_slice;
37: use function array_splice;
38: use function array_values;
39: use function count;
40: use function get_class;
41: use function implode;
42: use function in_array;
43: use function is_int;
44: use function sprintf;
45: use function usort;
46: use const PHP_INT_MAX;
47: use const PHP_INT_MIN;
48:
49: /**
50: * @api
51: */
52: final class TypeCombinator
53: {
54:
55: public static function addNull(Type $type): Type
56: {
57: $nullType = new NullType();
58:
59: if ($nullType->isSuperTypeOf($type)->no()) {
60: return self::union($type, $nullType);
61: }
62:
63: return $type;
64: }
65:
66: public static function remove(Type $fromType, Type $typeToRemove): Type
67: {
68: if ($typeToRemove instanceof UnionType) {
69: foreach ($typeToRemove->getTypes() as $unionTypeToRemove) {
70: $fromType = self::remove($fromType, $unionTypeToRemove);
71: }
72: return $fromType;
73: }
74:
75: $isSuperType = $typeToRemove->isSuperTypeOf($fromType);
76: if ($isSuperType->yes()) {
77: return new NeverType();
78: }
79: if ($isSuperType->no()) {
80: return $fromType;
81: }
82:
83: if ($typeToRemove instanceof MixedType) {
84: $typeToRemoveSubtractedType = $typeToRemove->getSubtractedType();
85: if ($typeToRemoveSubtractedType !== null) {
86: return self::intersect($fromType, $typeToRemoveSubtractedType);
87: }
88: }
89:
90: $removed = $fromType->tryRemove($typeToRemove);
91: if ($removed !== null) {
92: return $removed;
93: }
94:
95: $fromFiniteTypes = $fromType->getFiniteTypes();
96: if (count($fromFiniteTypes) > 0) {
97: $finiteTypesToRemove = $typeToRemove->getFiniteTypes();
98: if (count($finiteTypesToRemove) > 0) {
99: $result = [];
100: foreach ($fromFiniteTypes as $finiteType) {
101: foreach ($finiteTypesToRemove as $finiteTypeToRemove) {
102: if ($finiteType->equals($finiteTypeToRemove)) {
103: continue 2;
104: }
105: }
106:
107: $result[] = $finiteType;
108: }
109:
110: if (count($result) === count($fromFiniteTypes)) {
111: return $fromType;
112: }
113:
114: if (count($result) === 0) {
115: return new NeverType();
116: }
117:
118: if (count($result) === 1) {
119: return $result[0];
120: }
121:
122: return new UnionType($result);
123: }
124: }
125:
126: return $fromType;
127: }
128:
129: public static function removeNull(Type $type): Type
130: {
131: if (self::containsNull($type)) {
132: return self::remove($type, new NullType());
133: }
134:
135: return $type;
136: }
137:
138: public static function containsNull(Type $type): bool
139: {
140: if ($type instanceof UnionType) {
141: foreach ($type->getTypes() as $innerType) {
142: if ($innerType instanceof NullType) {
143: return true;
144: }
145: }
146:
147: return false;
148: }
149:
150: return $type instanceof NullType;
151: }
152:
153: public static function union(Type ...$types): Type
154: {
155: $typesCount = count($types);
156: if ($typesCount === 0) {
157: return new NeverType();
158: }
159:
160: // Fast path for single non-union type
161: if ($typesCount === 1) {
162: $singleType = $types[0];
163: if (!$singleType instanceof UnionType && !$singleType->isArray()->yes()) {
164: return $singleType;
165: }
166: }
167:
168: // Fast path for common 2-type cases
169: if ($typesCount === 2) {
170: $a = $types[0];
171: $b = $types[1];
172:
173: // union(never, X) = X and union(X, never) = X
174: if ($a instanceof NeverType && !$a->isExplicit()) {
175: return $b;
176: }
177: if ($b instanceof NeverType && !$b->isExplicit()) {
178: return $a;
179: }
180:
181: // union(mixed, X) = mixed (non-explicit, non-template, no subtracted)
182: if ($a instanceof MixedType && !$a->isExplicitMixed() && !$a instanceof TemplateMixedType && $a->getSubtractedType() === null) {
183: return $a;
184: }
185: if ($b instanceof MixedType && !$b->isExplicitMixed() && !$b instanceof TemplateMixedType && $b->getSubtractedType() === null) {
186: return $b;
187: }
188:
189: // union(X, X) = X (same object identity)
190: if ($a === $b) {
191: return $a;
192: }
193: }
194:
195: $alreadyNormalized = [];
196: $alreadyNormalizedCounter = 0;
197:
198: $benevolentTypes = [];
199: $benevolentUnionObject = null;
200: $neverCount = 0;
201: // transform A | (B | C) to A | B | C
202: for ($i = 0; $i < $typesCount; $i++) {
203: if (
204: $types[$i] instanceof MixedType
205: && !$types[$i]->isExplicitMixed()
206: && !$types[$i] instanceof TemplateMixedType
207: && $types[$i]->getSubtractedType() === null
208: ) {
209: return $types[$i];
210: }
211: if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
212: $neverCount++;
213: continue;
214: }
215: if ($types[$i] instanceof BenevolentUnionType) {
216: if ($types[$i] instanceof TemplateBenevolentUnionType && $benevolentUnionObject === null) {
217: $benevolentUnionObject = $types[$i];
218: }
219: $benevolentTypesCount = 0;
220: $typesInner = $types[$i]->getTypes();
221: foreach ($typesInner as $benevolentInnerType) {
222: $benevolentTypesCount++;
223: $benevolentTypes[$benevolentInnerType->describe(VerbosityLevel::value())] = $benevolentInnerType;
224: }
225: array_splice($types, $i, 1, $typesInner);
226: $typesCount += $benevolentTypesCount - 1;
227: continue;
228: }
229: if (!($types[$i] instanceof UnionType)) {
230: continue;
231: }
232: if ($types[$i] instanceof TemplateType) {
233: continue;
234: }
235:
236: $typesInner = $types[$i]->getTypes();
237: $alreadyNormalized[$alreadyNormalizedCounter] = $typesInner;
238: $alreadyNormalizedCounter++;
239: array_splice($types, $i, 1, $typesInner);
240: $typesCount += count($typesInner) - 1;
241: }
242:
243: // Bulk-remove implicit NeverTypes (skipped during the loop above)
244: if ($neverCount > 0) {
245: if ($neverCount === $typesCount) {
246: return new NeverType();
247: }
248:
249: $filtered = [];
250: for ($i = 0; $i < $typesCount; $i++) {
251: if ($types[$i] instanceof NeverType && !$types[$i]->isExplicit()) {
252: continue;
253: }
254: $filtered[] = $types[$i];
255: }
256: $types = $filtered;
257: $typesCount = count($types);
258:
259: if ($typesCount === 0) {
260: return new NeverType();
261: }
262: if ($typesCount === 1 && !$types[0]->isArray()->yes()) {
263: return $types[0];
264: }
265: if ($typesCount === 2) {
266: return self::union($types[0], $types[1]);
267: }
268: }
269:
270: if ($typesCount === 0) {
271: return new NeverType();
272: }
273:
274: if ($typesCount === 1 && !$types[0]->isArray()->yes()) {
275: return $types[0];
276: }
277:
278: $arrayTypes = [];
279: $scalarTypes = [];
280: $hasGenericScalarTypes = [];
281: $enumCaseTypes = [];
282: $integerRangeTypes = [];
283: for ($i = 0; $i < $typesCount; $i++) {
284: if ($types[$i]->isConstantScalarValue()->yes()) {
285: $type = $types[$i];
286: $scalarTypes[get_class($type)][$type->describe(VerbosityLevel::cache())] = $type;
287: unset($types[$i]);
288: continue;
289: }
290:
291: if ($types[$i]->isBoolean()->yes()) {
292: $hasGenericScalarTypes[ConstantBooleanType::class] = true;
293: } elseif ($types[$i]->isFloat()->yes()) {
294: $hasGenericScalarTypes[ConstantFloatType::class] = true;
295: } elseif ($types[$i]->isInteger()->yes() && !$types[$i] instanceof IntegerRangeType) {
296: $hasGenericScalarTypes[ConstantIntegerType::class] = true;
297: } elseif ($types[$i]->isString()->yes() && $types[$i]->isClassString()->no() && TypeUtils::getAccessoryTypes($types[$i]) === []) {
298: $hasGenericScalarTypes[ConstantStringType::class] = true;
299: } else {
300: $enumCase = $types[$i]->getEnumCaseObject();
301: if ($enumCase !== null) {
302: $enumCaseTypes[$types[$i]->describe(VerbosityLevel::cache())] = $types[$i];
303:
304: unset($types[$i]);
305: continue;
306: }
307: }
308:
309: if ($types[$i] instanceof IntegerRangeType) {
310: $integerRangeTypes[] = $types[$i];
311: unset($types[$i]);
312:
313: continue;
314: }
315:
316: if (!$types[$i]->isArray()->yes()) {
317: continue;
318: }
319:
320: $arrayTypes[] = $types[$i];
321: unset($types[$i]);
322: }
323:
324: $enumCaseTypes = array_values($enumCaseTypes);
325: usort(
326: $integerRangeTypes,
327: static fn (IntegerRangeType $a, IntegerRangeType $b): int => ($a->getMin() ?? PHP_INT_MIN) <=> ($b->getMin() ?? PHP_INT_MIN)
328: ?: ($a->getMax() ?? PHP_INT_MAX) <=> ($b->getMax() ?? PHP_INT_MAX),
329: );
330: $types = array_merge($types, $integerRangeTypes);
331: $types = array_values($types);
332: $typesCount = count($types);
333:
334: foreach ($scalarTypes as $classType => $scalarTypeItems) {
335: if (isset($hasGenericScalarTypes[$classType])) {
336: unset($scalarTypes[$classType]);
337: continue;
338: }
339: if ($classType === ConstantBooleanType::class && count($scalarTypeItems) === 2) {
340: $types[] = new BooleanType();
341: $typesCount++;
342: unset($scalarTypes[$classType]);
343: continue;
344: }
345:
346: $scalarTypeItems = array_values($scalarTypeItems);
347: $scalarTypeItemsCount = count($scalarTypeItems);
348: for ($i = 0; $i < $typesCount; $i++) {
349: for ($j = 0; $j < $scalarTypeItemsCount; $j++) {
350: $compareResult = self::compareTypesInUnion($types[$i], $scalarTypeItems[$j]);
351: if ($compareResult === null) {
352: continue;
353: }
354:
355: [$a, $b] = $compareResult;
356: if ($a !== null) {
357: $types[$i] = $a;
358: array_splice($scalarTypeItems, $j, 1);
359: $scalarTypeItemsCount--;
360: $j = -1;
361: continue 1;
362: }
363: if ($b !== null) {
364: $scalarTypeItems[$j] = $b;
365: array_splice($types, $i--, 1);
366: $typesCount--;
367: continue 2;
368: }
369: }
370: }
371:
372: $scalarTypes[$classType] = $scalarTypeItems;
373: }
374:
375: if (count($types) > 16) {
376: $newTypes = [];
377: foreach ($types as $type) {
378: $newTypes[$type->describe(VerbosityLevel::cache())] = $type;
379: }
380: $types = array_values($newTypes);
381: }
382:
383: $types = array_merge(
384: $types,
385: self::processArrayTypes($arrayTypes),
386: );
387: $typesCount = count($types);
388:
389: // transform A | A to A
390: // transform A | never to A
391: for ($i = 0; $i < $typesCount; $i++) {
392: for ($j = $i + 1; $j < $typesCount; $j++) {
393: if (self::isAlreadyNormalized($alreadyNormalized, $types[$i], $types[$j])) {
394: continue;
395: }
396: $compareResult = self::compareTypesInUnion($types[$i], $types[$j]);
397: if ($compareResult === null) {
398: continue;
399: }
400:
401: [$a, $b] = $compareResult;
402: if ($a !== null) {
403: $types[$i] = $a;
404: array_splice($types, $j--, 1);
405: $typesCount--;
406: continue 1;
407: }
408: if ($b !== null) {
409: $types[$j] = $b;
410: array_splice($types, $i--, 1);
411: $typesCount--;
412: continue 2;
413: }
414: }
415: }
416:
417: $enumCasesCount = count($enumCaseTypes);
418: for ($i = 0; $i < $typesCount; $i++) {
419: for ($j = 0; $j < $enumCasesCount; $j++) {
420: $compareResult = self::compareTypesInUnion($types[$i], $enumCaseTypes[$j]);
421: if ($compareResult === null) {
422: continue;
423: }
424:
425: [$a, $b] = $compareResult;
426: if ($a !== null) {
427: $types[$i] = $a;
428: array_splice($enumCaseTypes, $j--, 1);
429: $enumCasesCount--;
430: continue 1;
431: }
432: if ($b !== null) {
433: $enumCaseTypes[$j] = $b;
434: array_splice($types, $i--, 1);
435: $typesCount--;
436: continue 2;
437: }
438: }
439: }
440:
441: foreach ($enumCaseTypes as $enumCaseType) {
442: $types[] = $enumCaseType;
443: $typesCount++;
444: }
445:
446: foreach ($scalarTypes as $scalarTypeItems) {
447: foreach ($scalarTypeItems as $scalarType) {
448: $types[] = $scalarType;
449: $typesCount++;
450: }
451: }
452:
453: if ($typesCount === 0) {
454: return new NeverType();
455: }
456: if ($typesCount === 1) {
457: return $types[0];
458: }
459:
460: if ($benevolentTypes !== []) {
461: $tempTypes = $types;
462: foreach ($tempTypes as $i => $type) {
463: if (!isset($benevolentTypes[$type->describe(VerbosityLevel::value())])) {
464: break;
465: }
466:
467: unset($tempTypes[$i]);
468: }
469:
470: if ($tempTypes === []) {
471: if ($benevolentUnionObject instanceof TemplateBenevolentUnionType) {
472: return $benevolentUnionObject->withTypes(array_values($types));
473: }
474:
475: return new BenevolentUnionType(array_values($types), true);
476: }
477: }
478:
479: return new UnionType(array_values($types), true);
480: }
481:
482: /**
483: * @param array<int, Type[]> $alreadyNormalized
484: */
485: private static function isAlreadyNormalized(array $alreadyNormalized, Type $a, Type $b): bool
486: {
487: foreach ($alreadyNormalized as $normalizedTypes) {
488: foreach ($normalizedTypes as $i => $normalizedType) {
489: if ($normalizedType !== $a) {
490: continue;
491: }
492:
493: foreach ($normalizedTypes as $j => $anotherNormalizedType) {
494: if ($i === $j) {
495: continue;
496: }
497: if ($anotherNormalizedType === $b) {
498: return true;
499: }
500: }
501: }
502: }
503:
504: return false;
505: }
506:
507: /**
508: * @return array{Type, null}|array{null, Type}|null
509: */
510: private static function compareTypesInUnion(Type $a, Type $b): ?array
511: {
512: if ($a instanceof IntegerRangeType) {
513: $type = $a->tryUnion($b);
514: if ($type !== null) {
515: $a = $type;
516: return [$a, null];
517: }
518: }
519: if ($b instanceof IntegerRangeType) {
520: $type = $b->tryUnion($a);
521: if ($type !== null) {
522: $b = $type;
523: return [null, $b];
524: }
525: }
526: if ($a instanceof IntegerRangeType && $b instanceof IntegerRangeType) {
527: return null;
528: }
529: if ($a instanceof HasOffsetValueType && $b instanceof HasOffsetValueType) {
530: if ($a->getOffsetType()->equals($b->getOffsetType())) {
531: return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null];
532: }
533: }
534: if ($a instanceof IntersectionType && $b instanceof IntersectionType) {
535: $merged = self::mergeIntersectionsForUnion($a, $b);
536: if ($merged !== null) {
537: return [$merged, null];
538: }
539: }
540: if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) {
541: return null;
542: }
543:
544: // simplify string[] | int[] to (string|int)[]
545: if ($a instanceof IterableType && $b instanceof IterableType) {
546: return [
547: new IterableType(
548: self::union($a->getIterableKeyType(), $b->getIterableKeyType()),
549: self::union($a->getIterableValueType(), $b->getIterableValueType()),
550: ),
551: null,
552: ];
553: }
554:
555: if ($a instanceof SubtractableType) {
556: $typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType();
557: if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) {
558: $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($b);
559: } else {
560: $isSuperType = $typeWithoutSubtractedTypeA->isSuperTypeOf($b);
561: }
562: if ($isSuperType->yes()) {
563: $a = self::intersectWithSubtractedType($a, $b);
564: return [$a, null];
565: }
566: }
567:
568: if ($b instanceof SubtractableType) {
569: $typeWithoutSubtractedTypeB = $b->getTypeWithoutSubtractedType();
570: if ($typeWithoutSubtractedTypeB instanceof MixedType && $a instanceof MixedType) {
571: $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($a);
572: } else {
573: $isSuperType = $typeWithoutSubtractedTypeB->isSuperTypeOf($a);
574: }
575: if ($isSuperType->yes()) {
576: $b = self::intersectWithSubtractedType($b, $a);
577: return [null, $b];
578: }
579: }
580:
581: if ($b->isSuperTypeOf($a)->yes()) {
582: return [null, $b];
583: }
584:
585: if ($a->isSuperTypeOf($b)->yes()) {
586: return [$a, null];
587: }
588:
589: if (
590: $a instanceof ConstantStringType
591: ) {
592: if ($a->getValue() === '') {
593: $description = $b->describe(VerbosityLevel::value());
594: if (in_array($description, ['non-empty-string', 'non-falsy-string'], true)) {
595: return [null, self::intersect(
596: new StringType(),
597: ...self::getAccessoryCaseStringTypes($b),
598: )];
599: }
600: }
601:
602: if ($a->getValue() === '0') {
603: $description = $b->describe(VerbosityLevel::value());
604: if ($description === 'non-falsy-string') {
605: return [null, new IntersectionType([
606: new StringType(),
607: new AccessoryNonEmptyStringType(),
608: ...self::getAccessoryCaseStringTypes($b),
609: ])];
610: }
611: }
612: }
613:
614: if (
615: $b instanceof ConstantStringType
616: ) {
617: if ($b->getValue() === '') {
618: $description = $a->describe(VerbosityLevel::value());
619: if (in_array($description, ['non-empty-string', 'non-falsy-string'], true)) {
620: return [self::intersect(
621: new StringType(),
622: ...self::getAccessoryCaseStringTypes($a),
623: ), null];
624: }
625: }
626:
627: if ($b->getValue() === '0') {
628: $description = $a->describe(VerbosityLevel::value());
629: if ($description === 'non-falsy-string') {
630: return [new IntersectionType([
631: new StringType(),
632: new AccessoryNonEmptyStringType(),
633: ...self::getAccessoryCaseStringTypes($a),
634: ]), null];
635: }
636: }
637: }
638:
639: // numeric-string | non-decimal-int-string → string (preserving common accessories)
640: // Works because decimal-int-string ⊂ numeric-string, so together they cover all strings
641: if ($a->isString()->yes() && $b->isString()->yes()) {
642: $decimalIntString = new IntersectionType([new StringType(), new AccessoryDecimalIntegerStringType()]);
643: if ($b->isDecimalIntegerString()->no()) {
644: $bBase = self::removeDecimalIntStringAccessory($b);
645: if ($bBase->isSuperTypeOf($a)->yes() && $a->isSuperTypeOf($decimalIntString)->yes()) {
646: return [null, $bBase];
647: }
648: }
649: if ($a->isDecimalIntegerString()->no()) {
650: $aBase = self::removeDecimalIntStringAccessory($a);
651: if ($aBase->isSuperTypeOf($b)->yes() && $b->isSuperTypeOf($decimalIntString)->yes()) {
652: return [$aBase, null];
653: }
654: }
655: }
656:
657: return null;
658: }
659:
660: /**
661: * @return list<Type>
662: */
663: private static function getAccessoryCaseStringTypes(Type $type): array
664: {
665: $accessory = [];
666: if ($type->isLowercaseString()->yes()) {
667: $accessory[] = new AccessoryLowercaseStringType();
668: }
669: if ($type->isUppercaseString()->yes()) {
670: $accessory[] = new AccessoryUppercaseStringType();
671: }
672:
673: return $accessory;
674: }
675:
676: private static function removeDecimalIntStringAccessory(Type $type): Type
677: {
678: if (!$type instanceof IntersectionType) {
679: return $type;
680: }
681:
682: return self::intersect(...array_filter(
683: $type->getTypes(),
684: static fn (Type $t): bool => !$t instanceof AccessoryDecimalIntegerStringType,
685: ));
686: }
687:
688: private static function unionWithSubtractedType(
689: Type $type,
690: ?Type $subtractedType,
691: ): Type
692: {
693: if ($subtractedType === null) {
694: return $type;
695: }
696:
697: if ($subtractedType instanceof SubtractableType) {
698: $withoutSubtracted = $subtractedType->getTypeWithoutSubtractedType();
699: if ($withoutSubtracted->isSuperTypeOf($type)->yes()) {
700: $subtractedSubtractedType = $subtractedType->getSubtractedType();
701: if ($subtractedSubtractedType === null) {
702: return new NeverType();
703: }
704:
705: return self::intersect($type, $subtractedSubtractedType);
706: }
707: }
708:
709: if ($type instanceof SubtractableType) {
710: $subtractedType = $type->getSubtractedType() === null
711: ? $subtractedType
712: : self::union($type->getSubtractedType(), $subtractedType);
713:
714: $subtractedType = self::intersect(
715: $type->getTypeWithoutSubtractedType(),
716: $subtractedType,
717: );
718: if ($subtractedType instanceof NeverType) {
719: $subtractedType = null;
720: }
721:
722: return $type->changeSubtractedType($subtractedType);
723: }
724:
725: if ($subtractedType->isSuperTypeOf($type)->yes()) {
726: return new NeverType();
727: }
728:
729: return self::remove($type, $subtractedType);
730: }
731:
732: private static function intersectWithSubtractedType(
733: SubtractableType $a,
734: Type $b,
735: ): Type
736: {
737: if ($a->getSubtractedType() === null || $b instanceof NeverType) {
738: return $a;
739: }
740:
741: if ($b instanceof IntersectionType) {
742: $subtractableTypes = [];
743: foreach ($b->getTypes() as $innerType) {
744: if (!$innerType instanceof SubtractableType) {
745: continue;
746: }
747:
748: $subtractableTypes[] = $innerType;
749: }
750:
751: if (count($subtractableTypes) === 0) {
752: return $a->getTypeWithoutSubtractedType();
753: }
754:
755: $subtractedTypes = [];
756: foreach ($subtractableTypes as $subtractableType) {
757: if ($subtractableType->getSubtractedType() === null) {
758: continue;
759: }
760:
761: $subtractedTypes[] = $subtractableType->getSubtractedType();
762: }
763:
764: if (count($subtractedTypes) === 0) {
765: return $a->getTypeWithoutSubtractedType();
766:
767: }
768:
769: $subtractedType = self::union(...$subtractedTypes);
770: } else {
771: $isBAlreadySubtracted = $a->getSubtractedType()->isSuperTypeOf($b);
772:
773: if ($isBAlreadySubtracted->no()) {
774: return $a;
775: } elseif ($isBAlreadySubtracted->yes()) {
776: $subtractedType = self::remove($a->getSubtractedType(), $b);
777:
778: if (
779: $subtractedType instanceof NeverType
780: || !$subtractedType->isSuperTypeOf($b)->no()
781: ) {
782: $subtractedType = null;
783: }
784:
785: return $a->changeSubtractedType($subtractedType);
786: } elseif ($b instanceof SubtractableType) {
787: $subtractedType = $b->getSubtractedType();
788: if ($subtractedType === null) {
789: return $a->getTypeWithoutSubtractedType();
790: }
791: } else {
792: $subtractedTypeTmp = self::intersect($a->getTypeWithoutSubtractedType(), $a->getSubtractedType());
793: if ($b->isSuperTypeOf($subtractedTypeTmp)->yes()) {
794: return $a->getTypeWithoutSubtractedType();
795: }
796: $subtractedType = new MixedType(subtractedType: $b);
797: }
798: }
799:
800: $subtractedType = self::intersect(
801: $a->getSubtractedType(),
802: $subtractedType,
803: );
804: if ($subtractedType instanceof NeverType) {
805: $subtractedType = null;
806: }
807:
808: return $a->changeSubtractedType($subtractedType);
809: }
810:
811: /**
812: * @param Type[] $arrayTypes
813: * @return list<Type>
814: */
815: private static function processArrayAccessoryTypes(array $arrayTypes): array
816: {
817: $isIterableAtLeastOnce = [];
818: $accessoryTypes = [];
819: foreach ($arrayTypes as $i => $arrayType) {
820: $isIterableAtLeastOnce[] = $arrayType->isIterableAtLeastOnce();
821:
822: if ($arrayType instanceof IntersectionType) {
823: foreach ($arrayType->getTypes() as $innerType) {
824: if ($innerType instanceof TemplateType) {
825: break;
826: }
827: if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) {
828: continue;
829: }
830: if ($innerType instanceof HasOffsetType) {
831: $innerType = new HasOffsetValueType($innerType->getOffsetType(), $arrayType->getIterableValueType());
832: }
833: if ($innerType instanceof HasOffsetValueType) {
834: $accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType;
835: continue;
836: }
837:
838: $accessoryTypes[$innerType->describe(VerbosityLevel::cache())][$i] = $innerType;
839: }
840: }
841:
842: if (!$arrayType->isConstantArray()->yes()) {
843: continue;
844: }
845: $constantArrays = $arrayType->getConstantArrays();
846:
847: foreach ($constantArrays as $constantArray) {
848: if ($constantArray->isList()->yes()) {
849: $list = new AccessoryArrayListType();
850: $accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list;
851: }
852:
853: if (!$constantArray->isIterableAtLeastOnce()->yes()) {
854: continue;
855: }
856:
857: $nonEmpty = new NonEmptyArrayType();
858: $accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty;
859: }
860: }
861:
862: $commonAccessoryTypes = [];
863: $arrayTypeCount = count($arrayTypes);
864: foreach ($accessoryTypes as $accessoryType) {
865: if (count($accessoryType) !== $arrayTypeCount) {
866: $firstKey = array_key_first($accessoryType);
867: if ($accessoryType[$firstKey] instanceof OversizedArrayType) {
868: $commonAccessoryTypes[] = $accessoryType[$firstKey];
869: }
870: continue;
871: }
872:
873: if ($accessoryType[0] instanceof HasOffsetValueType) {
874: $commonAccessoryTypes[] = self::union(...$accessoryType);
875: continue;
876: }
877:
878: $commonAccessoryTypes[] = $accessoryType[0];
879: }
880:
881: if (TrinaryLogic::createYes()->and(...$isIterableAtLeastOnce)->yes()) {
882: $commonAccessoryTypes[] = new NonEmptyArrayType();
883: }
884:
885: return $commonAccessoryTypes;
886: }
887:
888: /**
889: * @param list<Type> $arrayTypes
890: * @return Type[]
891: */
892: private static function processArrayTypes(array $arrayTypes): array
893: {
894: if ($arrayTypes === []) {
895: return [];
896: }
897:
898: $accessoryTypes = self::processArrayAccessoryTypes($arrayTypes);
899:
900: if (count($arrayTypes) === 1) {
901: return [
902: self::intersect(...$arrayTypes, ...$accessoryTypes),
903: ];
904: }
905:
906: $keyTypesForGeneralArray = [];
907: $valueTypesForGeneralArray = [];
908: $generalArrayOccurred = false;
909: $constantKeyTypesNumbered = [];
910: $filledArrays = 0;
911: $overflowed = false;
912:
913: /** @var int|float $nextConstantKeyTypeIndex */
914: $nextConstantKeyTypeIndex = 1;
915:
916: foreach ($arrayTypes as $arrayType) {
917: $constantArrays = $arrayType->getConstantArrays();
918: $isConstantArray = $constantArrays !== [];
919: if (!$isConstantArray || !$arrayType->isIterableAtLeastOnce()->no()) {
920: $filledArrays++;
921: }
922:
923: if (!$isConstantArray) {
924: foreach ($arrayType->getArrays() as $type) {
925: $keyTypesForGeneralArray[] = $type->getIterableKeyType();
926: $valueTypesForGeneralArray[] = $type->getItemType();
927: $generalArrayOccurred = true;
928: }
929: continue;
930: }
931:
932: foreach ($constantArrays as $constantArray) {
933: $valueTypes = $constantArray->getValueTypes();
934: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
935: $valueTypesForGeneralArray[] = $valueTypes[$i];
936:
937: $keyTypeValue = $keyType->getValue();
938: if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) {
939: continue;
940: }
941: $keyTypesForGeneralArray[] = $keyType;
942:
943: $constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex;
944: $nextConstantKeyTypeIndex *= 2;
945: if (!is_int($nextConstantKeyTypeIndex)) {
946: $generalArrayOccurred = true;
947: $overflowed = true;
948: continue 2;
949: }
950: }
951: }
952: }
953:
954: if ($generalArrayOccurred && (!$overflowed || $filledArrays > 1)) {
955: $reducedArrayTypes = self::reduceArrays($arrayTypes, false);
956: if (count($reducedArrayTypes) === 1) {
957: return [self::intersect($reducedArrayTypes[0], ...$accessoryTypes)];
958: }
959:
960: $templateArrayType = null;
961: foreach ($arrayTypes as $arrayType) {
962: if (!$arrayType instanceof TemplateArrayType) {
963: $templateArrayType = null;
964: break;
965: }
966:
967: if ($templateArrayType !== null) {
968: continue;
969: }
970:
971: $templateArrayType = $arrayType;
972: }
973:
974: $arrayType = new ArrayType(
975: self::union(...$keyTypesForGeneralArray),
976: self::union(...self::optimizeConstantArrays($valueTypesForGeneralArray)),
977: );
978:
979: if ($templateArrayType !== null) {
980: $arrayType = new TemplateArrayType(
981: $templateArrayType->getScope(),
982: $templateArrayType->getStrategy(),
983: $templateArrayType->getVariance(),
984: $templateArrayType->getName(),
985: $arrayType,
986: $templateArrayType->getDefault(),
987: );
988: }
989:
990: return [
991: self::intersect($arrayType, ...$accessoryTypes),
992: ];
993: }
994:
995: $reducedArrayTypes = self::optimizeConstantArrays(self::reduceArrays($arrayTypes, true));
996: foreach ($reducedArrayTypes as $idx => $reducedArray) {
997: $applied = $accessoryTypes;
998: if ($reducedArray->isIterableAtLeastOnce()->no()) {
999: // Empty arrays cannot satisfy non-empty / oversized constraints —
1000: // applying those accessories would produce a contradictory intersection
1001: // (e.g. `array{}&oversized-array`) that rejects the very value it
1002: // represents, breaking the super-type contract of the union.
1003: $applied = array_values(array_filter(
1004: $applied,
1005: static fn (Type $t): bool => !($t instanceof OversizedArrayType) && !($t instanceof NonEmptyArrayType),
1006: ));
1007: }
1008: $reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$applied);
1009: }
1010: return $reducedArrayTypes;
1011: }
1012:
1013: /**
1014: * @param Type[] $types
1015: * @return Type[]
1016: */
1017: private static function optimizeConstantArrays(array $types): array
1018: {
1019: $constantArrayValuesCount = self::countConstantArrayValueTypes($types);
1020:
1021: if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1022: return $types;
1023: }
1024:
1025: // Stage 1: collapse same-key-set ConstantArrayType variants per-position
1026: // before the (lossy) generalization below kicks in. Variants with the
1027: // same key signature mergeWith losslessly into a single shape whose
1028: // values at each position are the union of the variants' values, which
1029: // drops the count while keeping the per-position structure. Without
1030: // this, a list of N similarly-shaped records (e.g. bug-7963) hits the
1031: // limit and the generalization decomposes every nested constant array
1032: // into a flat `non-empty-list<unionOfAllPositionValues>`, losing the
1033: // shape entirely.
1034: $signatureGroups = [];
1035: $nonConstantTypes = [];
1036: foreach ($types as $idx => $type) {
1037: if (!$type instanceof ConstantArrayType) {
1038: $nonConstantTypes[$idx] = $type;
1039: continue;
1040: }
1041: $signatureParts = [];
1042: $signatureParts[] = $type->isList()->yes() ? 'L' : 'A';
1043: foreach ($type->getKeyTypes() as $i => $keyType) {
1044: $signatureParts[] = ($type->isOptionalKey($i) ? '?' : '!') . ($keyType instanceof ConstantIntegerType ? 'i' : 's') . $keyType->getValue();
1045: }
1046: $signatureGroups[implode(',', $signatureParts)][] = $type;
1047: }
1048: if ($signatureGroups !== []) {
1049: $collapsed = $nonConstantTypes;
1050: $anyMerged = false;
1051: foreach ($signatureGroups as $group) {
1052: if (count($group) === 1) {
1053: $collapsed[] = $group[0];
1054: continue;
1055: }
1056: $merged = $group[0];
1057: for ($i = 1, $count = count($group); $i < $count; $i++) {
1058: $merged = $merged->mergeWith($group[$i]);
1059: }
1060: $collapsed[] = $merged;
1061: $anyMerged = true;
1062: }
1063: if ($anyMerged) {
1064: $types = array_values($collapsed);
1065: $constantArrayValuesCount = self::countConstantArrayValueTypes($types);
1066: if ($constantArrayValuesCount <= ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1067: return $types;
1068: }
1069: }
1070: }
1071:
1072: $results = [];
1073: $eachIsOversized = true;
1074: foreach ($types as $type) {
1075: $isOversized = false;
1076: $result = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$isOversized): Type {
1077: if (!$type instanceof ConstantArrayType) {
1078: return $traverse($type);
1079: }
1080:
1081: if ($type->isIterableAtLeastOnce()->no()) {
1082: return $type;
1083: }
1084:
1085: $isOversized = true;
1086:
1087: $isList = true;
1088: $valueTypes = [];
1089: $keyTypes = [];
1090: $nextAutoIndex = 0;
1091: $innerValueTypes = $type->getValueTypes();
1092: foreach ($type->getKeyTypes() as $i => $innerKeyType) {
1093: if (!$innerKeyType instanceof ConstantIntegerType) {
1094: $isList = false;
1095: } elseif ($innerKeyType->getValue() !== $nextAutoIndex) {
1096: $isList = false;
1097: $nextAutoIndex = $innerKeyType->getValue() + 1;
1098: } else {
1099: $nextAutoIndex++;
1100: }
1101:
1102: $generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific());
1103: $keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;
1104:
1105: // Inner traversal of the value position. Two subtleties, both
1106: // of which produced types that failed to be super-types of
1107: // their contributors:
1108: // - Empty constant arrays must be left alone; wrapping them
1109: // builds a contradictory `array{}&oversized-array`.
1110: // - Fall through via `$innerTraverse`, not the outer
1111: // `$traverse`. The outer callback fully generalizes a
1112: // sealed `ConstantArrayType` into `array<intKey, V>&...`,
1113: // which is correct at the top level but wrong inside a
1114: // value position: it would treat a sealed `array{a: 1}`
1115: // reached via `array{}|array{a: 1}` differently from one
1116: // reached directly, leaving `processArrayTypes` with a
1117: // mix of shapes it cannot unify cleanly.
1118: $generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type, callable $innerTraverse): Type {
1119: if ($type instanceof ConstantArrayType && $type->isIterableAtLeastOnce()->no()) {
1120: return $type;
1121: }
1122:
1123: if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
1124: return new IntersectionType([$type, new OversizedArrayType()]);
1125: }
1126:
1127: if ($type instanceof ConstantScalarType) {
1128: return $type->generalize(GeneralizePrecision::moreSpecific());
1129: }
1130:
1131: return $innerTraverse($type);
1132: });
1133: $valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
1134: }
1135:
1136: $keyType = TypeCombinator::union(...array_values($keyTypes));
1137: $valueType = TypeCombinator::union(...array_values($valueTypes));
1138:
1139: $accessories = [];
1140: if ($isList) {
1141: $accessories[] = new AccessoryArrayListType();
1142: }
1143: $accessories[] = new NonEmptyArrayType();
1144: $accessories[] = new OversizedArrayType();
1145:
1146: return self::intersect(new ArrayType($keyType, $valueType), ...$accessories);
1147: });
1148:
1149: if (!$isOversized) {
1150: $eachIsOversized = false;
1151: }
1152:
1153: $results[] = $result;
1154: }
1155:
1156: if ($eachIsOversized) {
1157: $eachIsList = true;
1158: $keyTypes = [];
1159: $valueTypes = [];
1160: foreach ($results as $result) {
1161: $keyTypes[] = $result->getIterableKeyType();
1162: $valueTypes[] = $result->getIterableValueType();
1163: if ($result->isList()->yes()) {
1164: continue;
1165: }
1166: $eachIsList = false;
1167: }
1168:
1169: $keyType = self::union(...$keyTypes);
1170: $valueType = self::union(...$valueTypes);
1171:
1172: if ($valueType instanceof UnionType && count($valueType->getTypes()) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1173: $valueType = $valueType->generalize(GeneralizePrecision::lessSpecific());
1174: }
1175:
1176: $accessories = [];
1177: if ($eachIsList) {
1178: $accessories[] = new AccessoryArrayListType();
1179: }
1180: $accessories[] = new NonEmptyArrayType();
1181: $accessories[] = new OversizedArrayType();
1182:
1183: return [self::intersect(new ArrayType($keyType, $valueType), ...$accessories)];
1184: }
1185:
1186: return $results;
1187: }
1188:
1189: /**
1190: * @param Type[] $types
1191: */
1192: public static function countConstantArrayValueTypes(array $types): int
1193: {
1194: $constantArrayValuesCount = 0;
1195: foreach ($types as $type) {
1196: TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$constantArrayValuesCount): Type {
1197: if ($type instanceof ConstantArrayType) {
1198: $constantArrayValuesCount += count($type->getValueTypes());
1199: }
1200:
1201: return $traverse($type);
1202: });
1203: }
1204: return $constantArrayValuesCount;
1205: }
1206:
1207: /**
1208: * @param list<Type> $constantArrays
1209: * @return list<Type>
1210: */
1211: private static function reduceArrays(array $constantArrays, bool $preserveTaggedUnions): array
1212: {
1213: $newArrays = [];
1214: $arraysToProcess = [];
1215: $emptyArray = null;
1216: foreach ($constantArrays as $constantArray) {
1217: if (!$constantArray->isConstantArray()->yes()) {
1218: // This is an optimization for current use-case of $preserveTaggedUnions=false, where we need
1219: // one constant array as a result, or we generalize the $constantArrays.
1220: if (!$preserveTaggedUnions) {
1221: return $constantArrays;
1222: }
1223: $newArrays[] = $constantArray;
1224: continue;
1225: }
1226:
1227: if ($constantArray->isIterableAtLeastOnce()->no()) {
1228: $emptyArray = $constantArray;
1229: continue;
1230: }
1231:
1232: $arraysToProcess = array_merge($arraysToProcess, $constantArray->getConstantArrays());
1233: }
1234:
1235: if ($emptyArray !== null) {
1236: if ($preserveTaggedUnions && $emptyArray instanceof ConstantArrayType) {
1237: // Let the empty array participate in merging — the passes below will absorb
1238: // it into any array that already accepts [] (all-optional keys, compatible
1239: // unsealed extras). If no such array exists, it remains as-is in the result.
1240: $arraysToProcess[] = $emptyArray;
1241: } else {
1242: $newArrays[] = $emptyArray;
1243: }
1244: }
1245:
1246: $arraysToProcessPerKey = [];
1247: foreach ($arraysToProcess as $i => $arrayToProcess) {
1248: foreach ($arrayToProcess->getKeyTypes() as $keyType) {
1249: $arraysToProcessPerKey[$keyType->getValue()][] = $i;
1250: }
1251: }
1252:
1253: $eligibleCombinations = [];
1254:
1255: foreach ($arraysToProcessPerKey as $arrays) {
1256: for ($i = 0, $arraysCount = count($arrays); $i < $arraysCount - 1; $i++) {
1257: for ($j = $i + 1; $j < $arraysCount; $j++) {
1258: $eligibleCombinations[$arrays[$i]][$arrays[$j]] ??= 0;
1259: $eligibleCombinations[$arrays[$i]][$arrays[$j]]++;
1260: }
1261: }
1262: }
1263:
1264: foreach ($eligibleCombinations as $i => $other) {
1265: if (!array_key_exists($i, $arraysToProcess)) {
1266: continue;
1267: }
1268:
1269: foreach ($other as $j => $overlappingKeysCount) {
1270: if (!array_key_exists($j, $arraysToProcess)) {
1271: continue;
1272: }
1273:
1274: // Merge two single-key arrays sharing the same key when their value
1275: // types union into a single type (not a UnionType). This is lossless
1276: // and prevents exponential union growth when narrowing nested
1277: // ArrayDimFetch expressions on a ConstantArrayType parent (see
1278: // phpstan/phpstan#14462).
1279: if (
1280: $preserveTaggedUnions
1281: && $overlappingKeysCount === 1
1282: && count($arraysToProcess[$i]->getKeyTypes()) === 1
1283: && count($arraysToProcess[$j]->getKeyTypes()) === 1
1284: ) {
1285: $iValueType = $arraysToProcess[$i]->getValueTypes()[0];
1286: $jValueType = $arraysToProcess[$j]->getValueTypes()[0];
1287: $unionValueType = self::union($iValueType, $jValueType);
1288: if (!$unionValueType instanceof UnionType) {
1289: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1290: unset($arraysToProcess[$i]);
1291: continue 2;
1292: }
1293: }
1294:
1295: if (
1296: $preserveTaggedUnions
1297: && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1298: && $arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])
1299: ) {
1300: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1301: unset($arraysToProcess[$i]);
1302: continue 2;
1303: }
1304:
1305: if (
1306: $preserveTaggedUnions
1307: && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1308: && $arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])
1309: ) {
1310: $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1311: unset($arraysToProcess[$j]);
1312: continue 1;
1313: }
1314:
1315: if (
1316: !$preserveTaggedUnions
1317: // both arrays have same keys
1318: && $overlappingKeysCount === count($arraysToProcess[$i]->getKeyTypes())
1319: && $overlappingKeysCount === count($arraysToProcess[$j]->getKeyTypes())
1320: ) {
1321: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1322: unset($arraysToProcess[$i]);
1323: continue 2;
1324: }
1325: }
1326: }
1327:
1328: // Second pass: merge pairs that the eligibleCombinations loop above couldn't touch.
1329: // That loop only considers pairs sharing at least one known key, so it never fires
1330: // for e.g. `array{}` ∪ `array{a?: 1}` (disjoint, one empty) or for two
1331: // unsealed-extras arrays with disjoint required keys. Both collapse losslessly if
1332: // one side's extras or optional-key shape can absorb the other side's content.
1333: //
1334: // Performance: two sealed, non-empty, no-extras arrays with disjoint keys cannot
1335: // merge losslessly (legacyIsKeysSupersetOf returns false immediately on the first
1336: // missing key). Skip those pairs via a candidate flag to avoid an O(n²) scan that
1337: // dominated analyse time on files accumulating many sealed ConstantArrayType
1338: // variants (bug-7581 / bug-8146a). A pair is worth checking only if at least one
1339: // side is (a) empty, or (b) has real unsealed extras, or (c) has optional keys —
1340: // the last case covers the narrowing shape used by e.g. array_key_exists checks
1341: // over large optional-key shapes (bug-14032).
1342: $indices = array_keys($arraysToProcess);
1343: $indicesCount = count($indices);
1344: if ($indicesCount > 1) {
1345: $candidateFlags = [];
1346: foreach ($indices as $idx) {
1347: $arr = $arraysToProcess[$idx];
1348: $unsealed = $arr->getUnsealedTypes();
1349: if ($unsealed === null) {
1350: $candidateFlags[$idx] = false;
1351: continue;
1352: }
1353: [$unsealedKey] = $unsealed;
1354: $hasRealExtras = !($unsealedKey instanceof NeverType && $unsealedKey->isExplicit());
1355: if ($hasRealExtras) {
1356: $candidateFlags[$idx] = true;
1357: continue;
1358: }
1359: $keyTypesCount = count($arr->getKeyTypes());
1360: if ($keyTypesCount === 0) {
1361: $candidateFlags[$idx] = true;
1362: continue;
1363: }
1364: $hasOptional = count($arr->getOptionalKeys()) > 0;
1365: $candidateFlags[$idx] = $hasOptional;
1366: }
1367:
1368: for ($ii = 0; $ii < $indicesCount - 1; $ii++) {
1369: $i = $indices[$ii];
1370: if (!array_key_exists($i, $arraysToProcess)) {
1371: continue;
1372: }
1373: if ($arraysToProcess[$i]->getUnsealedTypes() === null) {
1374: continue;
1375: }
1376: for ($jj = $ii + 1; $jj < $indicesCount; $jj++) {
1377: $j = $indices[$jj];
1378: if (!array_key_exists($j, $arraysToProcess)) {
1379: continue;
1380: }
1381: if (!$candidateFlags[$i] && !$candidateFlags[$j]) {
1382: continue;
1383: }
1384: if ($arraysToProcess[$j]->getUnsealedTypes() === null) {
1385: continue;
1386: }
1387: if ($arraysToProcess[$j]->isKeysSupersetOf($arraysToProcess[$i])) {
1388: $arraysToProcess[$j] = $arraysToProcess[$j]->mergeWith($arraysToProcess[$i]);
1389: unset($arraysToProcess[$i]);
1390: continue 2;
1391: }
1392: if (!$arraysToProcess[$i]->isKeysSupersetOf($arraysToProcess[$j])) {
1393: continue;
1394: }
1395:
1396: $arraysToProcess[$i] = $arraysToProcess[$i]->mergeWith($arraysToProcess[$j]);
1397: unset($arraysToProcess[$j]);
1398: }
1399: }
1400: }
1401:
1402: // Final pass: if merging left us with a ConstantArrayType that has no known keys
1403: // but has real unsealed extras, collapse it to a plain ArrayType (mirrors the same
1404: // logic in ConstantArrayTypeBuilder::getArray — but applies to results produced by
1405: // ConstantArrayType::mergeWith, which doesn't go through the builder).
1406: foreach ($arraysToProcess as $idx => $arr) {
1407: if (count($arr->getKeyTypes()) !== 0) {
1408: continue;
1409: }
1410: $unsealed = $arr->getUnsealedTypes();
1411: if ($unsealed === null) {
1412: continue;
1413: }
1414: [$unsealedKey, $unsealedValue] = $unsealed;
1415: if ($unsealedKey instanceof NeverType && $unsealedKey->isExplicit()) {
1416: continue;
1417: }
1418: $newArrays[] = new ArrayType($unsealedKey, $unsealedValue);
1419: unset($arraysToProcess[$idx]);
1420: }
1421:
1422: // Final pass: collapse the loop-accumulator pattern where each iteration
1423: // produced a longer non-empty list variant. When several non-empty list
1424: // ConstantArrayTypes survive earlier merging and together push the
1425: // constant-array value count past the limit, fold them into a single
1426: // non-empty-list<unionValueType> so the result stays bounded without
1427: // going through the lossier optimizeConstantArrays generalization.
1428: // Skip when every list variant shares one key signature — those collapse
1429: // losslessly via the stage 1 same-key-set merge in optimizeConstantArrays
1430: // (each position keeps its own value union), which is strictly more
1431: // precise than this flat fold.
1432: if ($preserveTaggedUnions && count($arraysToProcess) > 1) {
1433: $listVariantIndices = [];
1434: $listValueTypes = [];
1435: $listVariants = [];
1436: $listVariantSignatures = [];
1437: foreach ($arraysToProcess as $idx => $arr) {
1438: if (!$arr->isList()->yes() || !$arr->isIterableAtLeastOnce()->yes()) {
1439: continue;
1440: }
1441: $listVariantIndices[] = $idx;
1442: $listValueTypes[] = $arr->getIterableValueType();
1443: $listVariants[] = $arr;
1444: $signatureParts = [];
1445: foreach ($arr->getKeyTypes() as $i => $keyType) {
1446: $signatureParts[] = ($arr->isOptionalKey($i) ? '?' : '!') . ($keyType instanceof ConstantIntegerType ? 'i' : 's') . $keyType->getValue();
1447: }
1448: $listVariantSignatures[implode(',', $signatureParts)] = true;
1449: }
1450: if (
1451: count($listVariantIndices) >= 2
1452: && count($listVariantSignatures) >= 2
1453: && self::countConstantArrayValueTypes($listVariants) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1454: ) {
1455: $mergedValueType = self::union(...$listValueTypes);
1456: $merged = self::intersect(
1457: new ArrayType(new IntegerType(), $mergedValueType),
1458: new NonEmptyArrayType(),
1459: new AccessoryArrayListType(),
1460: );
1461: $newArrays[] = $merged;
1462: foreach ($listVariantIndices as $idx) {
1463: unset($arraysToProcess[$idx]);
1464: }
1465: }
1466: }
1467:
1468: return array_merge($newArrays, $arraysToProcess);
1469: }
1470:
1471: public static function intersect(Type ...$types): Type
1472: {
1473: $typesCount = count($types);
1474: if ($typesCount === 0) {
1475: return new NeverType();
1476: }
1477:
1478: $types = array_values($types);
1479: if ($typesCount === 1) {
1480: return $types[0];
1481: }
1482:
1483: foreach ($types as $type) {
1484: if ($type instanceof NeverType && !$type->isExplicit()) {
1485: return $type;
1486: }
1487: }
1488:
1489: $sortTypes = static function (Type $a, Type $b): int {
1490: if (!$a instanceof UnionType || !$b instanceof UnionType) {
1491: return 0;
1492: }
1493:
1494: if ($a instanceof TemplateType) {
1495: return -1;
1496: }
1497: if ($b instanceof TemplateType) {
1498: return 1;
1499: }
1500:
1501: if ($a instanceof BenevolentUnionType) {
1502: return -1;
1503: }
1504: if ($b instanceof BenevolentUnionType) {
1505: return 1;
1506: }
1507:
1508: return 0;
1509: };
1510: usort($types, $sortTypes);
1511: // transform A & (B | C) to (A & B) | (A & C)
1512: foreach ($types as $i => $type) {
1513: if (!$type instanceof UnionType) {
1514: continue;
1515: }
1516:
1517: $topLevelUnionSubTypes = [];
1518: $innerTypes = $type->getTypes();
1519: usort($innerTypes, $sortTypes);
1520: $slice1 = array_slice($types, 0, $i);
1521: $slice2 = array_slice($types, $i + 1);
1522: foreach ($innerTypes as $innerUnionSubType) {
1523: $topLevelUnionSubTypes[] = self::intersect(
1524: $innerUnionSubType,
1525: ...$slice1,
1526: ...$slice2,
1527: );
1528: }
1529:
1530: $union = self::union(...$topLevelUnionSubTypes);
1531: if ($union instanceof NeverType) {
1532: return $union;
1533: }
1534:
1535: if ($type instanceof BenevolentUnionType) {
1536: $union = TypeUtils::toBenevolentUnion($union);
1537: }
1538:
1539: if ($type instanceof TemplateUnionType || $type instanceof TemplateBenevolentUnionType) {
1540: $union = TemplateTypeFactory::create(
1541: $type->getScope(),
1542: $type->getName(),
1543: $union,
1544: $type->getVariance(),
1545: $type->getStrategy(),
1546: $type->getDefault(),
1547: );
1548: }
1549:
1550: return $union;
1551: }
1552:
1553: $newTypes = [];
1554: $hasOffsetValueTypeCount = 0;
1555: $typesCount = count($types);
1556: for ($i = 0; $i < $typesCount; $i++) {
1557: $type = $types[$i];
1558:
1559: if ($type instanceof IntersectionType && !$type instanceof TemplateType) {
1560: // transform A & (B & C) to A & B & C
1561: array_splice($types, $i--, 1, $type->getTypes());
1562: $typesCount = count($types);
1563: } elseif ($type instanceof HasOffsetValueType) {
1564: $hasOffsetValueTypeCount++;
1565: } else {
1566: $newTypes[] = $type;
1567: }
1568: }
1569:
1570: if ($hasOffsetValueTypeCount > 32) {
1571: $newTypes[] = new OversizedArrayType();
1572: $types = $newTypes;
1573: $typesCount = count($types);
1574: }
1575:
1576: usort($types, static function (Type $a, Type $b): int {
1577: // move subtractables with subtracts before those without to avoid losing them in the union logic
1578: if ($a instanceof SubtractableType && $a->getSubtractedType() !== null) {
1579: return -1;
1580: }
1581: if ($b instanceof SubtractableType && $b->getSubtractedType() !== null) {
1582: return 1;
1583: }
1584:
1585: if ($a instanceof ConstantArrayType && !$b instanceof ConstantArrayType) {
1586: return -1;
1587: }
1588: if ($b instanceof ConstantArrayType && !$a instanceof ConstantArrayType) {
1589: return 1;
1590: }
1591:
1592: return 0;
1593: });
1594:
1595: // transform IntegerType & ConstantIntegerType to ConstantIntegerType
1596: // transform Child & Parent to Child
1597: // transform Object & ~null to Object
1598: // transform A & A to A
1599: // transform int[] & string to never
1600: // transform callable & int to never
1601: // transform A & ~A to never
1602: // transform int & string to never
1603: for ($i = 0; $i < $typesCount; $i++) {
1604: for ($j = $i + 1; $j < $typesCount; $j++) {
1605: if ($types[$j] instanceof SubtractableType) {
1606: $typeWithoutSubtractedTypeA = $types[$j]->getTypeWithoutSubtractedType();
1607:
1608: if ($typeWithoutSubtractedTypeA instanceof MixedType && $types[$i] instanceof MixedType) {
1609: $isSuperTypeSubtractableA = $typeWithoutSubtractedTypeA->isSuperTypeOfMixed($types[$i]);
1610: } else {
1611: $isSuperTypeSubtractableA = $typeWithoutSubtractedTypeA->isSuperTypeOf($types[$i]);
1612: }
1613: if ($isSuperTypeSubtractableA->yes()) {
1614: $types[$i] = self::unionWithSubtractedType($types[$i], $types[$j]->getSubtractedType());
1615: array_splice($types, $j--, 1);
1616: $typesCount--;
1617: continue 1;
1618: }
1619: }
1620:
1621: if ($types[$i] instanceof SubtractableType) {
1622: $typeWithoutSubtractedTypeB = $types[$i]->getTypeWithoutSubtractedType();
1623:
1624: if ($typeWithoutSubtractedTypeB instanceof MixedType && $types[$j] instanceof MixedType) {
1625: $isSuperTypeSubtractableB = $typeWithoutSubtractedTypeB->isSuperTypeOfMixed($types[$j]);
1626: } else {
1627: $isSuperTypeSubtractableB = $typeWithoutSubtractedTypeB->isSuperTypeOf($types[$j]);
1628: }
1629: if ($isSuperTypeSubtractableB->yes()) {
1630: $types[$j] = self::unionWithSubtractedType($types[$j], $types[$i]->getSubtractedType());
1631: array_splice($types, $i--, 1);
1632: $typesCount--;
1633: continue 2;
1634: }
1635: }
1636:
1637: if ($types[$i] instanceof IntegerRangeType) {
1638: $intersectionType = $types[$i]->tryIntersect($types[$j]);
1639: if ($intersectionType !== null) {
1640: $types[$j] = $intersectionType;
1641: array_splice($types, $i--, 1);
1642: $typesCount--;
1643: continue 2;
1644: }
1645: }
1646:
1647: if ($types[$j] instanceof IterableType) {
1648: $isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]);
1649: } else {
1650: $isSuperTypeA = $types[$j]->isSuperTypeOf($types[$i]);
1651: }
1652:
1653: if ($isSuperTypeA->yes()) {
1654: array_splice($types, $j--, 1);
1655: $typesCount--;
1656: continue;
1657: }
1658:
1659: if ($types[$i] instanceof IterableType) {
1660: $isSuperTypeB = $types[$i]->isSuperTypeOfMixed($types[$j]);
1661: } else {
1662: $isSuperTypeB = $types[$i]->isSuperTypeOf($types[$j]);
1663: }
1664:
1665: if ($isSuperTypeB->maybe()) {
1666: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetType) {
1667: $types[$i] = $types[$i]->makeOffsetRequired($types[$j]->getOffsetType());
1668: array_splice($types, $j--, 1);
1669: $typesCount--;
1670: continue;
1671: }
1672:
1673: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetType) {
1674: $types[$j] = $types[$j]->makeOffsetRequired($types[$i]->getOffsetType());
1675: array_splice($types, $i--, 1);
1676: $typesCount--;
1677: continue 2;
1678: }
1679:
1680: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof AccessoryArrayListType) {
1681: $types[$i] = $types[$i]->makeList();
1682: array_splice($types, $j--, 1);
1683: $typesCount--;
1684: continue;
1685: }
1686:
1687: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof AccessoryArrayListType) {
1688: $types[$j] = $types[$j]->makeList();
1689: array_splice($types, $i--, 1);
1690: $typesCount--;
1691: continue 2;
1692: }
1693:
1694: if (
1695: $types[$i] instanceof ConstantArrayType
1696: && $types[$j] instanceof NonEmptyArrayType
1697: && (count($types[$i]->getKeyTypes()) === 1 || $types[$i]->isList()->yes())
1698: && $types[$i]->isOptionalKey(0)
1699: && !$types[$i]->isUnsealed()->yes()
1700: ) {
1701: $types[$i] = $types[$i]->makeOffsetRequired($types[$i]->getKeyTypes()[0]);
1702: array_splice($types, $j--, 1);
1703: $typesCount--;
1704: continue;
1705: }
1706:
1707: if (
1708: $types[$j] instanceof ConstantArrayType
1709: && $types[$i] instanceof NonEmptyArrayType
1710: && (count($types[$j]->getKeyTypes()) === 1 || $types[$j]->isList()->yes())
1711: && $types[$j]->isOptionalKey(0)
1712: && !$types[$j]->isUnsealed()->yes()
1713: ) {
1714: $types[$j] = $types[$j]->makeOffsetRequired($types[$j]->getKeyTypes()[0]);
1715: array_splice($types, $i--, 1);
1716: $typesCount--;
1717: continue 2;
1718: }
1719:
1720: if ($types[$i] instanceof ConstantArrayType && $types[$j] instanceof HasOffsetValueType) {
1721: $offsetType = $types[$j]->getOffsetType();
1722: $valueType = $types[$j]->getValueType();
1723: $newValueType = self::intersect($types[$i]->getOffsetValueType($offsetType), $valueType);
1724: if ($newValueType instanceof NeverType) {
1725: return $newValueType;
1726: }
1727: $types[$i] = $types[$i]->setOffsetValueType($offsetType, $newValueType);
1728: array_splice($types, $j--, 1);
1729: $typesCount--;
1730: continue;
1731: }
1732:
1733: if ($types[$j] instanceof ConstantArrayType && $types[$i] instanceof HasOffsetValueType) {
1734: $offsetType = $types[$i]->getOffsetType();
1735: $valueType = $types[$i]->getValueType();
1736: $newValueType = self::intersect($types[$j]->getOffsetValueType($offsetType), $valueType);
1737: if ($newValueType instanceof NeverType) {
1738: return $newValueType;
1739: }
1740:
1741: $types[$j] = $types[$j]->setOffsetValueType($offsetType, $newValueType);
1742: array_splice($types, $i--, 1);
1743: $typesCount--;
1744: continue 2;
1745: }
1746:
1747: if ($types[$i] instanceof OversizedArrayType && $types[$j] instanceof HasOffsetValueType) {
1748: array_splice($types, $j--, 1);
1749: $typesCount--;
1750: continue;
1751: }
1752:
1753: if ($types[$j] instanceof OversizedArrayType && $types[$i] instanceof HasOffsetValueType) {
1754: array_splice($types, $i--, 1);
1755: $typesCount--;
1756: continue 2;
1757: }
1758:
1759: if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
1760: $types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName());
1761: array_splice($types, $j--, 1);
1762: $typesCount--;
1763: continue;
1764: }
1765:
1766: if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
1767: $types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName());
1768: array_splice($types, $i--, 1);
1769: $typesCount--;
1770: continue 2;
1771: }
1772:
1773: $constArrayIsI = $types[$i] instanceof ConstantArrayType && ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType);
1774: $constArrayIsJ = $types[$j] instanceof ConstantArrayType && ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType);
1775: if ($constArrayIsI || $constArrayIsJ) {
1776: $constArray = $constArrayIsI ? $types[$i] : $types[$j];
1777: $otherArray = $constArrayIsI ? $types[$j] : $types[$i];
1778:
1779: if (
1780: $otherArray instanceof ConstantArrayType
1781: && !$constArray->isUnsealed()->maybe()
1782: && !$otherArray->isUnsealed()->maybe()
1783: ) {
1784: $merged = self::intersectDefiniteConstantArrays($constArray, $otherArray);
1785: if ($merged instanceof NeverType) {
1786: return $merged;
1787: }
1788: $newArrayType = $merged;
1789: } else {
1790: $newArray = ConstantArrayTypeBuilder::createEmpty();
1791: // Preserve unsealed extras from the source shape so the
1792: // rebuild doesn't silently turn `array{k: int, ...} & X`
1793: // into a sealed `array{k: int}` — intersect with the other
1794: // side's iterable key/value so the open part keeps both
1795: // sides' refinements.
1796: $constUnsealed = $constArray->getUnsealedTypes();
1797: if ($constUnsealed !== null && $constArray->isUnsealed()->yes()) {
1798: $newUnsealedKey = self::intersect($constUnsealed[0], $otherArray->getIterableKeyType());
1799: $newUnsealedValue = self::intersect($constUnsealed[1], $otherArray->getIterableValueType());
1800: if (!$newUnsealedKey instanceof NeverType && !$newUnsealedValue instanceof NeverType) {
1801: $newArray->makeUnsealed($newUnsealedKey, $newUnsealedValue);
1802: }
1803: }
1804: $valueTypes = $constArray->getValueTypes();
1805: foreach ($constArray->getKeyTypes() as $k => $keyType) {
1806: $hasOffset = $otherArray->hasOffsetValueType($keyType);
1807: if ($hasOffset->no()) {
1808: continue;
1809: }
1810: $newArray->setOffsetValueType(
1811: self::intersect($keyType, $otherArray->getIterableKeyType()),
1812: self::intersect($valueTypes[$k], $otherArray->getOffsetValueType($keyType)),
1813: $constArray->isOptionalKey($k) && !$hasOffset->yes(),
1814: );
1815: }
1816: $newArrayType = $newArray->getArray();
1817: }
1818:
1819: if ($constArrayIsI) {
1820: $types[$i] = $newArrayType;
1821: array_splice($types, $j--, 1);
1822: } else {
1823: $types[$j] = $newArrayType;
1824: array_splice($types, $i--, 1);
1825: }
1826: $typesCount--;
1827: continue 2;
1828: }
1829:
1830: if (
1831: ($types[$i] instanceof ArrayType || $types[$i] instanceof ConstantArrayType || $types[$i] instanceof IterableType) &&
1832: ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType || $types[$j] instanceof IterableType)
1833: ) {
1834: $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getKeyType());
1835: $itemType = self::intersect($types[$i]->getItemType(), $types[$j]->getItemType());
1836: if ($types[$i] instanceof IterableType && $types[$j] instanceof IterableType) {
1837: $types[$j] = new IterableType($keyType, $itemType);
1838: } else {
1839: $types[$j] = new ArrayType($keyType, $itemType);
1840: }
1841: array_splice($types, $i--, 1);
1842: $typesCount--;
1843: continue 2;
1844: }
1845:
1846: if ($types[$i] instanceof GenericClassStringType && $types[$j] instanceof GenericClassStringType) {
1847: $genericType = self::intersect($types[$i]->getGenericType(), $types[$j]->getGenericType());
1848: $types[$i] = new GenericClassStringType($genericType);
1849: array_splice($types, $j--, 1);
1850: $typesCount--;
1851: continue;
1852: }
1853:
1854: if (
1855: $types[$i] instanceof ArrayType
1856: && get_class($types[$i]) === ArrayType::class
1857: && $types[$j] instanceof AccessoryArrayListType
1858: && !$types[$j]->getIterableKeyType()->isSuperTypeOf($types[$i]->getIterableKeyType())->yes()
1859: ) {
1860: $keyType = self::intersect($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType());
1861: if ($keyType instanceof NeverType) {
1862: return $keyType;
1863: }
1864: $types[$i] = new ArrayType($keyType, $types[$i]->getItemType());
1865: continue;
1866: }
1867:
1868: continue;
1869: }
1870:
1871: if ($isSuperTypeB->yes()) {
1872: array_splice($types, $i--, 1);
1873: $typesCount--;
1874: continue 2;
1875: }
1876:
1877: if ($isSuperTypeA->no()) {
1878: return new NeverType();
1879: }
1880: }
1881: }
1882:
1883: if ($typesCount === 1) {
1884: return $types[0];
1885: }
1886:
1887: $accessoryBaseTypes = [];
1888: foreach ($types as $type) {
1889: if (!$type instanceof AccessoryType) {
1890: $accessoryBaseTypes = null;
1891: break;
1892: }
1893: $accessoryBaseTypes[] = $type->getDefaultBaseType();
1894: }
1895: if ($accessoryBaseTypes !== null) {
1896: // Accessory types never stand alone — supply the base type they refine.
1897: return self::intersect(self::intersect(...$accessoryBaseTypes), ...$types);
1898: }
1899:
1900: return new IntersectionType($types);
1901: }
1902:
1903: private static function intersectDefiniteConstantArrays(ConstantArrayType $a, ConstantArrayType $b): Type
1904: {
1905: $aSealed = $a->isUnsealed()->no();
1906: $bSealed = $b->isUnsealed()->no();
1907: $bothUnsealed = !$aSealed && !$bSealed && $a->getUnsealedTypes() !== null && $b->getUnsealedTypes() !== null;
1908:
1909: $aKeyByValue = [];
1910: foreach ($a->getKeyTypes() as $k => $keyType) {
1911: $aKeyByValue[$keyType->getValue()] = $k;
1912: }
1913: $bKeyByValue = [];
1914: foreach ($b->getKeyTypes() as $k => $keyType) {
1915: $bKeyByValue[$keyType->getValue()] = $k;
1916: }
1917:
1918: if ($aSealed && $bSealed) {
1919: foreach ($aKeyByValue as $keyValue => $k) {
1920: if (!$a->isOptionalKey($k) && !array_key_exists($keyValue, $bKeyByValue)) {
1921: return new NeverType();
1922: }
1923: }
1924: foreach ($bKeyByValue as $keyValue => $k) {
1925: if (!$b->isOptionalKey($k) && !array_key_exists($keyValue, $aKeyByValue)) {
1926: return new NeverType();
1927: }
1928: }
1929: }
1930:
1931: $newArray = ConstantArrayTypeBuilder::createEmpty();
1932:
1933: if ($bothUnsealed) {
1934: $aUnsealed = $a->getUnsealedTypes();
1935: $bUnsealed = $b->getUnsealedTypes();
1936: $unsealedKey = self::intersect($aUnsealed[0], $bUnsealed[0]);
1937: $unsealedValue = self::intersect($aUnsealed[1], $bUnsealed[1]);
1938: if ($unsealedKey instanceof NeverType || $unsealedValue instanceof NeverType) {
1939: return new NeverType();
1940: }
1941: $newArray->makeUnsealed($unsealedKey, $unsealedValue);
1942: } else {
1943: $never = new NeverType(true);
1944: $newArray->makeUnsealed($never, $never);
1945: }
1946:
1947: $resolveOtherValue = static function (ConstantArrayType $other, Type $keyType): ?Type {
1948: if ($other->hasOffsetValueType($keyType)->yes()) {
1949: return $other->getOffsetValueType($keyType);
1950: }
1951: $otherUnsealed = $other->getUnsealedTypes();
1952: if ($otherUnsealed === null) {
1953: return null;
1954: }
1955: [$unsealedKey, $unsealedValue] = $otherUnsealed;
1956: if ($unsealedKey instanceof NeverType && $unsealedKey->isExplicit()) {
1957: return null;
1958: }
1959: if ($unsealedKey->isSuperTypeOf($keyType)->no()) {
1960: return null;
1961: }
1962: return $unsealedValue;
1963: };
1964:
1965: $keysToProcess = [];
1966: foreach ($aKeyByValue as $keyValue => $k) {
1967: $keysToProcess[$keyValue] = [$k, $bKeyByValue[$keyValue] ?? null];
1968: }
1969: foreach ($bKeyByValue as $keyValue => $k) {
1970: if (array_key_exists($keyValue, $keysToProcess)) {
1971: continue;
1972: }
1973:
1974: $keysToProcess[$keyValue] = [null, $k];
1975: }
1976:
1977: foreach ($keysToProcess as [$aIdx, $bIdx]) {
1978: if ($aIdx !== null && $bIdx !== null) {
1979: $keyType = $a->getKeyTypes()[$aIdx];
1980: $value = self::intersect($a->getValueTypes()[$aIdx], $b->getValueTypes()[$bIdx]);
1981: $optional = $a->isOptionalKey($aIdx) && $b->isOptionalKey($bIdx);
1982: } elseif ($aIdx !== null) {
1983: $keyType = $a->getKeyTypes()[$aIdx];
1984: $aValue = $a->getValueTypes()[$aIdx];
1985: $bValue = $resolveOtherValue($b, $keyType);
1986: if ($bValue === null) {
1987: if ($a->isOptionalKey($aIdx)) {
1988: continue;
1989: }
1990: return new NeverType();
1991: }
1992: $value = self::intersect($aValue, $bValue);
1993: $optional = $a->isOptionalKey($aIdx);
1994: } else {
1995: /** @var int<0, max> $bIdx */
1996: $keyType = $b->getKeyTypes()[$bIdx];
1997: $bValue = $b->getValueTypes()[$bIdx];
1998: $aValue = $resolveOtherValue($a, $keyType);
1999: if ($aValue === null) {
2000: if ($b->isOptionalKey($bIdx)) {
2001: continue;
2002: }
2003: return new NeverType();
2004: }
2005: $value = self::intersect($aValue, $bValue);
2006: $optional = $b->isOptionalKey($bIdx);
2007: }
2008:
2009: if ($value instanceof NeverType) {
2010: if ($optional) {
2011: continue;
2012: }
2013: return new NeverType();
2014: }
2015: $newArray->setOffsetValueType($keyType, $value, $optional);
2016: }
2017:
2018: return $newArray->getArray();
2019: }
2020:
2021: /**
2022: * Merge two IntersectionTypes that have the same structure but differ
2023: * in HasOffsetValueType value types (matched by offset key).
2024: *
2025: * E.g. (A & hasOV('k', X)) | (A & hasOV('k', Y)) → (A & hasOV('k', X|Y))
2026: */
2027: private static function mergeIntersectionsForUnion(IntersectionType $a, IntersectionType $b): ?Type
2028: {
2029: $aTypes = $a->getTypes();
2030: $bTypes = $b->getTypes();
2031:
2032: if (count($aTypes) !== count($bTypes)) {
2033: return null;
2034: }
2035:
2036: $mergedTypes = [];
2037: $hasDifference = false;
2038: $bUsed = array_fill(0, count($bTypes), false);
2039:
2040: foreach ($aTypes as $aType) {
2041: $matched = false;
2042: foreach ($bTypes as $bIdx => $bType) {
2043: if ($bUsed[$bIdx]) {
2044: continue;
2045: }
2046:
2047: if ($aType->equals($bType)) {
2048: $mergedTypes[] = $aType;
2049: $bUsed[$bIdx] = true;
2050: $matched = true;
2051: break;
2052: }
2053:
2054: // HasOffsetValueType: merge value types when offset keys match
2055: if ($aType instanceof HasOffsetValueType && $bType instanceof HasOffsetValueType
2056: && $aType->getOffsetType()->equals($bType->getOffsetType())) {
2057: $mergedTypes[] = new HasOffsetValueType(
2058: $aType->getOffsetType(),
2059: self::union($aType->getValueType(), $bType->getValueType()),
2060: );
2061: $hasDifference = true;
2062: $bUsed[$bIdx] = true;
2063: $matched = true;
2064: break;
2065: }
2066:
2067: // HasOffsetType, HasMethodType, HasPropertyType: only equal values match (no merging possible)
2068: }
2069: if (!$matched) {
2070: return null;
2071: }
2072: }
2073:
2074: if (!$hasDifference) {
2075: return null;
2076: }
2077:
2078: $result = $mergedTypes[0];
2079: for ($i = 1, $count = count($mergedTypes); $i < $count; $i++) {
2080: $result = self::intersect($result, $mergedTypes[$i]);
2081: }
2082: return $result;
2083: }
2084:
2085: public static function removeFalsey(Type $type): Type
2086: {
2087: return self::remove($type, StaticTypeFactory::falsey());
2088: }
2089:
2090: public static function removeTruthy(Type $type): Type
2091: {
2092: return self::remove($type, StaticTypeFactory::truthy());
2093: }
2094:
2095: }
2096: