Skip to content

Commit e2022ca

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Restrict list type preservation in IntersectionType::setOffsetValueType to offsets within list key range
- In `IntersectionType::setOffsetValueType`, change the condition for re-adding `AccessoryArrayListType` from checking if the offset is any integer (`$offsetType->toArrayKey()->isInteger()->yes()`) to checking if the offset is within the list's key type range (`$this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()`). - This prevents list type from being incorrectly preserved when assigning with an arbitrary `int` key (which includes negative values and could create gaps), while still preserving list type for `int<0, max>` offsets (valid list key range) used in nested modifications like `$list[$k]['key'] = value`. - Update bug-10089 test assertion: `$matrix[$size - 1][8] = 3` where `$size` is `int<min, 8>` correctly degrades the list since `int<min, 7>` is not within the `int<0, max>` key range.
1 parent 03834ec commit e2022ca

File tree

3 files changed

+110
-2
lines changed

3 files changed

+110
-2
lines changed

src/Type/IntersectionType.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -987,7 +987,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
987987
if (
988988
$this->isList()->yes()
989989
&& $offsetType !== null
990-
&& $offsetType->toArrayKey()->isInteger()->yes()
990+
&& $this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()
991991
&& $this->getIterableValueType()->isArray()->yes()
992992
) {
993993
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ protected function create_matrix(int $size): array
2424
$matrix[$size - 1][8] = 3;
2525

2626
// non-empty-array<int, non-empty-array<int, 0|3>&hasOffsetValue(8, 3)>
27-
assertType('non-empty-list<non-empty-array<int<0, max>, 0|3>>', $matrix);
27+
assertType('non-empty-array<int, non-empty-array<int<0, max>, 0|3>>', $matrix);
2828

2929
for ($i = 0; $i <= $size; $i++) {
3030
if ($matrix[$i][8] === 0) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Bug14336;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* Assigning with arbitrary int key in a loop should degrade list to array.
9+
*
10+
* @param list<array{abc: string}> $list
11+
* @param array<int, int> $intMap
12+
*/
13+
function testAssignAnyIntInLoop(array $list, array $intMap): void
14+
{
15+
foreach ($intMap as $intKey => $intValue) {
16+
$list[$intKey] = ['abc' => 'def'];
17+
}
18+
assertType("array<int, array{abc: string}>", $list);
19+
}
20+
21+
/**
22+
* @param list<string> $list
23+
* @param int $intKey
24+
*/
25+
function testAssignAnyIntOutsideLoop(array $list, int $intKey): void
26+
{
27+
$list[$intKey] = 'foo';
28+
assertType("non-empty-array<int, string>", $list);
29+
}
30+
31+
/**
32+
* Safe patterns should still preserve list.
33+
*
34+
* @param list<string> $list
35+
*/
36+
function testKeepListWithAppend(array $list): void
37+
{
38+
$list[] = 'foo';
39+
assertType("non-empty-list<string>", $list);
40+
}
41+
42+
/**
43+
* @param list<string> $list
44+
*/
45+
function testKeepListWithConstantZero(array $list): void
46+
{
47+
$list[0] = 'foo';
48+
assertType("non-empty-list<string>&hasOffsetValue(0, 'foo')", $list);
49+
}
50+
51+
/**
52+
* Nested array assignment in loop should keep outer list when key comes from iteration.
53+
*
54+
* @param list<array<string, string>> $list
55+
*/
56+
function testNestedAssignKeepsList(array $list): void
57+
{
58+
foreach ($list as $k => $v) {
59+
$list[$k]['abc'] = 'world';
60+
}
61+
assertType("list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);
62+
}
63+
64+
/**
65+
* @param list<list<string>> $list
66+
* @param int $intKey
67+
*/
68+
function testNestedListAssignWithAnyInt(array $list, int $intKey): void
69+
{
70+
$list[$intKey] = ['foo'];
71+
assertType("non-empty-array<int, list<string>>", $list);
72+
}
73+
74+
/**
75+
* Assigning with negative int key should also degrade list.
76+
*
77+
* @param list<string> $list
78+
* @param int<min, -1> $negativeKey
79+
*/
80+
function testAssignNegativeInt(array $list, int $negativeKey): void
81+
{
82+
$list[$negativeKey] = 'foo';
83+
assertType("non-empty-array<int, string>", $list);
84+
}
85+
86+
/**
87+
* Assigning with int<0, max> should still keep list (valid range).
88+
*
89+
* @param list<array<string>> $list
90+
* @param int<0, max> $nonNegativeKey
91+
*/
92+
function testAssignNonNegativeIntWithArrayValue(array $list, int $nonNegativeKey): void
93+
{
94+
$list[$nonNegativeKey] = ['foo'];
95+
assertType("non-empty-list<array<string>>", $list);
96+
}
97+
98+
/**
99+
* Direct scalar assignment with int<0, max> key.
100+
*
101+
* @param list<string> $list
102+
* @param int<0, max> $nonNegativeKey
103+
*/
104+
function testAssignNonNegativeIntWithScalarValue(array $list, int $nonNegativeKey): void
105+
{
106+
$list[$nonNegativeKey] = 'foo';
107+
assertType("non-empty-array<int<0, max>, string>", $list);
108+
}

0 commit comments

Comments
 (0)