Skip to content

Commit 429723d

Browse files
committed
Properly union HasOffsetValueType in huge intersections describing the same offsets
1 parent 07cfb98 commit 429723d

File tree

3 files changed

+69
-2
lines changed

3 files changed

+69
-2
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1716,7 +1716,7 @@ parameters:
17161716
-
17171717
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
17181718
identifier: phpstanApi.instanceofType
1719-
count: 3
1719+
count: 5
17201720
path: src/Type/TypeCombinator.php
17211721

17221722
-

src/Type/TypeCombinator.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\Type\Generic\TemplateType;
2727
use PHPStan\Type\Generic\TemplateTypeFactory;
2828
use PHPStan\Type\Generic\TemplateUnionType;
29+
use function array_fill;
2930
use function array_key_exists;
3031
use function array_key_first;
3132
use function array_merge;
@@ -463,6 +464,12 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
463464
return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null];
464465
}
465466
}
467+
if ($a instanceof IntersectionType && $b instanceof IntersectionType) {
468+
$merged = self::mergeIntersectionsForUnion($a, $b);
469+
if ($merged !== null) {
470+
return [$merged, null];
471+
}
472+
}
466473
if ($a->isConstantArray()->yes() && $b->isConstantArray()->yes()) {
467474
return null;
468475
}
@@ -1516,6 +1523,66 @@ public static function intersect(Type ...$types): Type
15161523
return new IntersectionType($types);
15171524
}
15181525

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+
15191586
public static function removeFalsey(Type $type): Type
15201587
{
15211588
return self::remove($type, StaticTypeFactory::falsey());

tests/PHPStan/Analyser/nsrt/bug-14319.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ protected function edit(int|string|null $IdNum = null): void
4343
if ($rows['rap_roz3']) {
4444
$raport .= 'Roz: '.$rows['rap_roz3'].", \n";
4545
}
46-
assertType("(non-empty-array&hasOffsetValue('rap_br', mixed)&hasOffsetValue('rap_ks', mixed)&hasOffsetValue('rap_tr', mixed))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))", $rows);
46+
assertType("(non-empty-array&hasOffsetValue('rap_br', mixed)&hasOffsetValue('rap_cz', mixed)&hasOffsetValue('rap_fil', mixed)&hasOffsetValue('rap_ks', mixed)&hasOffsetValue('rap_roz', mixed)&hasOffsetValue('rap_roz2', mixed)&hasOffsetValue('rap_roz3', mixed)&hasOffsetValue('rap_tr', mixed))|(ArrayAccess&hasOffsetValue('rap_br', mixed)&hasOffsetValue('rap_cz', mixed)&hasOffsetValue('rap_fil', mixed)&hasOffsetValue('rap_ks', mixed)&hasOffsetValue('rap_roz', mixed)&hasOffsetValue('rap_roz2', mixed)&hasOffsetValue('rap_roz3', mixed)&hasOffsetValue('rap_tr', mixed))", $rows);
4747
}
4848
}
4949
}

0 commit comments

Comments
 (0)