|
26 | 26 | use PHPStan\Type\Generic\TemplateType; |
27 | 27 | use PHPStan\Type\Generic\TemplateTypeFactory; |
28 | 28 | use PHPStan\Type\Generic\TemplateUnionType; |
| 29 | +use function array_fill; |
29 | 30 | use function array_key_exists; |
30 | 31 | use function array_key_first; |
31 | 32 | use function array_merge; |
@@ -463,6 +464,12 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array |
463 | 464 | return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null]; |
464 | 465 | } |
465 | 466 | } |
| 467 | + if ($a instanceof IntersectionType && $b instanceof IntersectionType) { |
| 468 | + $merged = self::mergeIntersectionsForUnion($a, $b); |
| 469 | + if ($merged !== null) { |
| 470 | + return [$merged, null]; |
| 471 | + } |
| 472 | + } |
466 | 473 | if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) { |
467 | 474 | return null; |
468 | 475 | } |
@@ -1516,6 +1523,66 @@ public static function intersect(Type ...$types): Type |
1516 | 1523 | return new IntersectionType($types); |
1517 | 1524 | } |
1518 | 1525 |
|
| 1526 | + /** |
| 1527 | + * Merge two IntersectionTypes that have the same structure but differ |
| 1528 | + * in HasOffsetValueType value types (matched by offset key). |
| 1529 | + * |
| 1530 | + * E.g. (A & hasOV('k', X)) | (A & hasOV('k', Y)) → (A & hasOV('k', X|Y)) |
| 1531 | + */ |
| 1532 | + private static function mergeIntersectionsForUnion(IntersectionType $a, IntersectionType $b): ?Type |
| 1533 | + { |
| 1534 | + $aTypes = $a->getTypes(); |
| 1535 | + $bTypes = $b->getTypes(); |
| 1536 | + |
| 1537 | + if (count($aTypes) !== count($bTypes)) { |
| 1538 | + return null; |
| 1539 | + } |
| 1540 | + |
| 1541 | + $mergedTypes = []; |
| 1542 | + $hasDifference = false; |
| 1543 | + $bUsed = array_fill(0, count($bTypes), false); |
| 1544 | + |
| 1545 | + foreach ($aTypes as $aType) { |
| 1546 | + $matched = false; |
| 1547 | + foreach ($bTypes as $bIdx => $bType) { |
| 1548 | + if ($bUsed[$bIdx]) { |
| 1549 | + continue; |
| 1550 | + } |
| 1551 | + |
| 1552 | + if ($aType->equals($bType)) { |
| 1553 | + $mergedTypes[] = $aType; |
| 1554 | + $bUsed[$bIdx] = true; |
| 1555 | + $matched = true; |
| 1556 | + break; |
| 1557 | + } |
| 1558 | + |
| 1559 | + // HasOffsetValueType: merge value types when offset keys match |
| 1560 | + if ($aType instanceof HasOffsetValueType && $bType instanceof HasOffsetValueType |
| 1561 | + && $aType->getOffsetType()->equals($bType->getOffsetType())) { |
| 1562 | + $mergedTypes[] = new HasOffsetValueType( |
| 1563 | + $aType->getOffsetType(), |
| 1564 | + self::union($aType->getValueType(), $bType->getValueType()), |
| 1565 | + ); |
| 1566 | + $hasDifference = true; |
| 1567 | + $bUsed[$bIdx] = true; |
| 1568 | + $matched = true; |
| 1569 | + break; |
| 1570 | + } |
| 1571 | + |
| 1572 | + // HasOffsetType, HasMethodType, HasPropertyType: only equal values match (no merging possible) |
| 1573 | + } |
| 1574 | + if (!$matched) { |
| 1575 | + return null; |
| 1576 | + } |
| 1577 | + } |
| 1578 | + |
| 1579 | + if (!$hasDifference) { |
| 1580 | + return null; |
| 1581 | + } |
| 1582 | + |
| 1583 | + return self::intersect(...$mergedTypes); |
| 1584 | + } |
| 1585 | + |
1519 | 1586 | public static function removeFalsey(Type $type): Type |
1520 | 1587 | { |
1521 | 1588 | return self::remove($type, StaticTypeFactory::falsey()); |
|
0 commit comments