Skip to content

Commit 56ecce5

Browse files
phpstan-botgithub-actions[bot]claude
authored
Use position-specific getOffsetValueType() instead of getIterableValueType() when intersecting two ConstantArrayTypes (#5468)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3d82a9c commit 56ecce5

File tree

3 files changed

+109
-4
lines changed

3 files changed

+109
-4
lines changed

src/Type/TypeCombinator.php

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,10 +1515,14 @@ public static function intersect(Type ...$types): Type
15151515
$newArray = ConstantArrayTypeBuilder::createEmpty();
15161516
$valueTypes = $types[$i]->getValueTypes();
15171517
foreach ($types[$i]->getKeyTypes() as $k => $keyType) {
1518+
$hasOffset = $types[$j]->hasOffsetValueType($keyType);
1519+
if ($hasOffset->no()) {
1520+
continue;
1521+
}
15181522
$newArray->setOffsetValueType(
15191523
self::intersect($keyType, $types[$j]->getIterableKeyType()),
1520-
self::intersect($valueTypes[$k], $types[$j]->getIterableValueType()),
1521-
$types[$i]->isOptionalKey($k) && !$types[$j]->hasOffsetValueType($keyType)->yes(),
1524+
self::intersect($valueTypes[$k], $types[$j]->getOffsetValueType($keyType)),
1525+
$types[$i]->isOptionalKey($k) && !$hasOffset->yes(),
15221526
);
15231527
}
15241528
$types[$i] = $newArray->getArray();
@@ -1531,10 +1535,14 @@ public static function intersect(Type ...$types): Type
15311535
$newArray = ConstantArrayTypeBuilder::createEmpty();
15321536
$valueTypes = $types[$j]->getValueTypes();
15331537
foreach ($types[$j]->getKeyTypes() as $k => $keyType) {
1538+
$hasOffset = $types[$i]->hasOffsetValueType($keyType);
1539+
if ($hasOffset->no()) {
1540+
continue;
1541+
}
15341542
$newArray->setOffsetValueType(
15351543
self::intersect($keyType, $types[$i]->getIterableKeyType()),
1536-
self::intersect($valueTypes[$k], $types[$i]->getIterableValueType()),
1537-
$types[$j]->isOptionalKey($k) && !$types[$i]->hasOffsetValueType($keyType)->yes(),
1544+
self::intersect($valueTypes[$k], $types[$i]->getOffsetValueType($keyType)),
1545+
$types[$j]->isOptionalKey($k) && !$hasOffset->yes(),
15381546
);
15391547
}
15401548
$types[$j] = $newArray->getArray();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php // lint >= 8.0
2+
3+
namespace Bug11234;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Payload {}
8+
9+
/** @param array{0|1|2|3, int|Payload|string|null}&array{int, Payload} $x */
10+
function testIntersectConstantUnionWithInt(mixed $x): void
11+
{
12+
assertType('array{0|1|2|3, Bug11234\Payload}', $x);
13+
}
14+
15+
/** @param array{int, Payload}&array{0|1|2|3, int|Payload|string|null} $x */
16+
function testIntersectConstantUnionWithIntReverse(mixed $x): void
17+
{
18+
assertType('array{0|1|2|3, Bug11234\Payload}', $x);
19+
}
20+
21+
/** @param array{0|1|2|3, int|Payload|string|null}&array{0|1|2|3, Payload} $x */
22+
function testIntersectBothConstantUnion(mixed $x): void
23+
{
24+
assertType('array{0|1|2|3, Bug11234\Payload}', $x);
25+
}
26+
27+
/** @param array{int, int|Payload|string|null}&array{int, Payload} $y */
28+
function testIntersectPlainInt(mixed $y): void
29+
{
30+
assertType('array{int, Bug11234\Payload}', $y);
31+
}
32+
33+
/** @param array{0|1, string|int, Payload|null}&array{int, string, Payload} $z */
34+
function testIntersectThreePositions(mixed $z): void
35+
{
36+
assertType('array{0|1, string, Bug11234\Payload}', $z);
37+
}
38+
39+
/** @param array{'a'|'b', int|Payload|string|null}&array{string, Payload} $w */
40+
function testIntersectStringConstantUnion(mixed $w): void
41+
{
42+
assertType("array{'a'|'b', Bug11234\Payload}", $w);
43+
}
44+
45+
/** @param array{0|1, int|string}&array{int, int, extra?: bool} $v */
46+
function testIntersectOptionalKey(mixed $v): void
47+
{
48+
assertType('array{0|1, int}', $v);
49+
}
50+
51+
/** @param array{true|false, int|string}&array{bool, string} $u */
52+
function testIntersectBoolConstantUnion(mixed $u): void
53+
{
54+
assertType('array{bool, string}', $u);
55+
}
56+
57+
/** @param array{int<0, 3>, int|Payload|string|null}&array{int, Payload} $x */
58+
function testIntersectIntRangeValue(mixed $x): void
59+
{
60+
assertType('array{int<0, 3>, Bug11234\Payload}', $x);
61+
}
62+
63+
/** @param array{non-empty-string, int|Payload|string|null}&array{string, Payload} $x */
64+
function testIntersectNonEmptyStringValue(mixed $x): void
65+
{
66+
assertType('array{non-empty-string, Bug11234\Payload}', $x);
67+
}
68+
69+
/** @param array{0|1|2|3, non-empty-string|int|null}&array{int, string} $x */
70+
function testIntersectNonEmptyStringInUnion(mixed $x): void
71+
{
72+
assertType('array{0|1|2|3, non-empty-string}', $x);
73+
}
74+
75+
/** @param array{0|1|2|3, string|null}&array{int, non-empty-string} $x */
76+
function testIntersectWithNonEmptyStringOtherSide(mixed $x): void
77+
{
78+
assertType('array{0|1|2|3, non-empty-string}', $x);
79+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4933,6 +4933,24 @@ public static function dataIntersect(): iterable
49334933
TemplateIntersectionType::class,
49344934
'T of Countable&Iterator (function a(), parameter)',
49354935
];
4936+
4937+
yield [
4938+
[
4939+
new ConstantArrayType(
4940+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
4941+
[
4942+
new UnionType([new ConstantIntegerType(0), new ConstantIntegerType(1), new ConstantIntegerType(2), new ConstantIntegerType(3)]),
4943+
new UnionType([new IntegerType(), new ObjectType('stdClass'), new StringType(), new NullType()]),
4944+
],
4945+
),
4946+
new ConstantArrayType(
4947+
[new ConstantIntegerType(0), new ConstantIntegerType(1)],
4948+
[new IntegerType(), new ObjectType('stdClass')],
4949+
),
4950+
],
4951+
ConstantArrayType::class,
4952+
'array{0|1|2|3, stdClass}',
4953+
];
49364954
}
49374955

49384956
/**

0 commit comments

Comments
 (0)