Skip to content

Commit 17beb01

Browse files
authored
Set offset on list keeps list if there's HasOffsetType for all preceeding offsets
1 parent 194ba99 commit 17beb01

File tree

2 files changed

+65
-2
lines changed

2 files changed

+65
-2
lines changed

src/Type/IntersectionType.php

+24-2
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,30 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
800800

801801
$result = $this->intersectTypes(static fn (Type $type): Type => $type->setOffsetValueType($offsetType, $valueType, $unionValues));
802802

803-
if ($offsetType !== null && $this->isList()->yes() && $this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
804-
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
803+
if (
804+
$offsetType !== null
805+
&& $this->isList()->yes()
806+
&& !$result->isList()->yes()
807+
) {
808+
if ($this->isIterableAtLeastOnce()->yes() && (new ConstantIntegerType(1))->isSuperTypeOf($offsetType)->yes()) {
809+
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
810+
} else {
811+
foreach ($this->types as $type) {
812+
if (!$type instanceof HasOffsetValueType && !$type instanceof HasOffsetType) {
813+
continue;
814+
}
815+
816+
foreach ($type->getOffsetType()->getConstantScalarValues() as $constantScalarValue) {
817+
if (!is_int($constantScalarValue)) {
818+
continue;
819+
}
820+
if (IntegerRangeType::fromInterval(0, $constantScalarValue + 1)->isSuperTypeOf($offsetType)->yes()) {
821+
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
822+
break 2;
823+
}
824+
}
825+
}
826+
}
805827
}
806828

807829
return $result;

tests/PHPStan/Analyser/nsrt/list-type.php

+41
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,45 @@ public function testSetOffsetExplicitlyWithGap(array $list): void
128128
assertType('non-empty-array<int<0, max>, int>&hasOffsetValue(0, 17)&hasOffsetValue(2, 21)', $list);
129129
}
130130

131+
/** @param list<int> $list */
132+
function testAppendImmediatelyAfterLastElement(array $list): void
133+
{
134+
assertType('list<int>', $list);
135+
$list[0] = 17;
136+
assertType('non-empty-list<int>&hasOffsetValue(0, 17)', $list);
137+
$list[1] = 19;
138+
assertType('non-empty-list<int>&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)', $list);
139+
$list[2] = 21;
140+
assertType('non-empty-list<int>&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)', $list);
141+
$list[3] = 21;
142+
assertType('non-empty-list<int>&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)&hasOffsetValue(3, 21)', $list);
143+
144+
// hole in the list -> turns it into a array
145+
146+
$list[5] = 21;
147+
assertType('non-empty-array<int<0, max>, int>&hasOffsetValue(0, 17)&hasOffsetValue(1, 19)&hasOffsetValue(2, 21)&hasOffsetValue(3, 21)&hasOffsetValue(5, 21)', $list);
148+
}
149+
150+
151+
/** @param list<int> $list */
152+
function testKeepListAfterLast(array $list): void
153+
{
154+
if (isset($list[5])) {
155+
assertType('non-empty-list<int>&hasOffsetValue(5, int)', $list);
156+
$list[6] = 21;
157+
assertType('non-empty-list<int>&hasOffsetValue(5, int)&hasOffsetValue(6, 21)', $list);
158+
}
159+
assertType('list<int>', $list);
160+
}
161+
162+
/** @param list<int> $list */
163+
function testKeepListAfterLastArrayKey(array $list): void
164+
{
165+
if (array_key_exists(5, $list) && is_int($list[5])) {
166+
assertType('non-empty-list<int>&hasOffsetValue(5, int)', $list);
167+
$list[6] = 21;
168+
assertType('non-empty-list<int>&hasOffsetValue(5, int)&hasOffsetValue(6, 21)', $list);
169+
}
170+
assertType('list<int>', $list);
171+
}
131172
}

0 commit comments

Comments
 (0)