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