Skip to content

Commit 6b6c9c4

Browse files
herndlmondrejmirtes
authored andcommitted
Fix array_slice() edge cases
1 parent adf032e commit 6b6c9c4

File tree

6 files changed

+58
-12
lines changed

6 files changed

+58
-12
lines changed

src/Type/Accessory/NonEmptyArrayType.php

+1-4
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,7 @@ public function shuffleArray(): Type
216216

217217
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
218218
{
219-
if (
220-
(new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
221-
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
222-
) {
219+
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes()) {
223220
return $this;
224221
}
225222

src/Type/ArrayType.php

+4
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,10 @@ public function shuffleArray(): Type
442442

443443
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
444444
{
445+
if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) {
446+
return new ConstantArrayType([], []);
447+
}
448+
445449
if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) {
446450
return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType());
447451
}

src/Type/Constant/ConstantArrayType.php

+15-5
Original file line numberDiff line numberDiff line change
@@ -944,17 +944,27 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
944944
->sliceArray($offsetType, $lengthType, $preserveKeys);
945945
}
946946

947+
if ($keyTypesCount + $offset <= 0) {
948+
// A negative offset cannot reach left outside the array twice
949+
$offset = 0;
950+
}
951+
952+
if ($keyTypesCount + $length <= 0) {
953+
// A negative length cannot reach left outside the array twice
954+
$length = 0;
955+
}
956+
957+
if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) {
958+
// 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything
959+
return new self([], []);
960+
}
961+
947962
if ($length < 0) {
948963
// Negative lengths prevent access to the most right n elements
949964
return $this->removeLastElements($length * -1)
950965
->sliceArray($offsetType, new NullType(), $preserveKeys);
951966
}
952967

953-
if ($keyTypesCount + $offset <= 0) {
954-
// A negative offset cannot reach left outside the array
955-
$offset = 0;
956-
}
957-
958968
if ($offset < 0) {
959969
/*
960970
* Transforms the problem with the negative offset in one with a positive offset using array reversion.

src/Type/IntersectionType.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,18 @@ public function shuffleArray(): Type
896896

897897
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
898898
{
899-
return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
899+
$result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
900+
901+
if (
902+
$this->isList()->yes()
903+
&& $this->isIterableAtLeastOnce()->yes()
904+
&& (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
905+
&& IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()
906+
) {
907+
$result = TypeCombinator::intersect($result, new NonEmptyArrayType());
908+
}
909+
910+
return $result;
900911
}
901912

902913
public function getEnumCases(): array

tests/PHPStan/Analyser/nsrt/array-slice.php

+24
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,22 @@ public function normalArrays(array $arr): void
3636
/** @var array<string, int> $arr */
3737
assertType('array<string, int>', array_slice($arr, 1, 2));
3838
assertType('array<string, int>', array_slice($arr, 1, 2, true));
39+
40+
/** @var non-empty-array<string> $arr */
41+
assertType('array{}', array_slice($arr, 0, 0));
42+
assertType('array{}', array_slice($arr, 0, 0, true));
43+
44+
/** @var non-empty-array<string> $arr */
45+
assertType('array<string>', array_slice($arr, 0, 1));
46+
assertType('array<string>', array_slice($arr, 0, 1, true));
47+
48+
/** @var list<string> $arr */
49+
assertType('list<string>', array_slice($arr, 0, 1));
50+
assertType('list<string>', array_slice($arr, 0, 1, true));
51+
52+
/** @var non-empty-list<string> $arr */
53+
assertType('non-empty-list<string>', array_slice($arr, 0, 1));
54+
assertType('non-empty-list<string>', array_slice($arr, 0, 1, true));
3955
}
4056

4157
public function constantArrays(array $arr): void
@@ -48,6 +64,14 @@ public function constantArrays(array $arr): void
4864
/** @var array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr */
4965
assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2));
5066
assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2, true));
67+
68+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
69+
assertType('array{}', array_slice($arr, -1, -1));
70+
assertType('array{}', array_slice($arr, -1, -1, true));
71+
72+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
73+
assertType('array{}', array_slice($arr, -1, -2));
74+
assertType('array{}', array_slice($arr, -1, -2, true));
5175
}
5276

5377
public function constantArraysWithOptionalKeys(array $arr): void

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public function doFoo()
1515
assertType('non-empty-array<0|1|2|3|4, 0|1|2|3|4>', $items);
1616
$batch = array_splice($items, 0, 2);
1717
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items);
18-
assertType('non-empty-list<0|1|2|3|4>', $batch);
18+
assertType('list<0|1|2|3|4>', $batch);
1919
}
2020
}
2121

@@ -28,7 +28,7 @@ public function doBar($items)
2828
assertType('non-empty-array<int>', $items);
2929
$batch = array_splice($items, 0, 2);
3030
assertType('array<int>', $items);
31-
assertType('non-empty-array<int>', $batch);
31+
assertType('array<int>', $batch);
3232
}
3333
}
3434

0 commit comments

Comments
 (0)