From 600dfad6cde614ef00bb4e0ec3e7770306fcd379 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Mon, 21 Apr 2025 20:37:54 +0200 Subject: [PATCH] Fix `array_slice()` edge cases --- src/Type/Accessory/NonEmptyArrayType.php | 5 +---- src/Type/ArrayType.php | 4 ++++ src/Type/Constant/ConstantArrayType.php | 20 ++++++++++++----- src/Type/IntersectionType.php | 13 ++++++++++- tests/PHPStan/Analyser/nsrt/array-slice.php | 24 +++++++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-5017.php | 4 ++-- 6 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index d4726fd5c2..2fbea580ca 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -216,10 +216,7 @@ public function shuffleArray(): Type public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - if ( - (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() - && ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes()) - ) { + if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes()) { return $this; } diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index e68a6a61d3..e6c0097db7 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -442,6 +442,10 @@ public function shuffleArray(): Type public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { + if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) { + return new ConstantArrayType([], []); + } + if ($preserveKeys->no() && $this->keyType->isInteger()->yes()) { return TypeCombinator::intersect(new self(new IntegerType(), $this->itemType), new AccessoryArrayListType()); } diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 8e76f0d08f..a24df69710 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -944,17 +944,27 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre ->sliceArray($offsetType, $lengthType, $preserveKeys); } + if ($keyTypesCount + $offset <= 0) { + // A negative offset cannot reach left outside the array twice + $offset = 0; + } + + if ($keyTypesCount + $length <= 0) { + // A negative length cannot reach left outside the array twice + $length = 0; + } + + if ($length === 0 || ($offset < 0 && $length < 0 && $offset - $length >= 0)) { + // 0 / 0, 3 / 0 or e.g. -3 / -3 or -3 / -4 and so on never extract anything + return new self([], []); + } + if ($length < 0) { // Negative lengths prevent access to the most right n elements return $this->removeLastElements($length * -1) ->sliceArray($offsetType, new NullType(), $preserveKeys); } - if ($keyTypesCount + $offset <= 0) { - // A negative offset cannot reach left outside the array - $offset = 0; - } - if ($offset < 0) { /* * Transforms the problem with the negative offset in one with a positive offset using array reversion. diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index 149536a573..89c00f26d2 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -896,7 +896,18 @@ public function shuffleArray(): Type public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type { - return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + $result = $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys)); + + if ( + $this->isList()->yes() + && $this->isIterableAtLeastOnce()->yes() + && (new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes() + ) { + $result = TypeCombinator::intersect($result, new NonEmptyArrayType()); + } + + return $result; } public function getEnumCases(): array diff --git a/tests/PHPStan/Analyser/nsrt/array-slice.php b/tests/PHPStan/Analyser/nsrt/array-slice.php index 12caf4fdbf..caf08c8d65 100644 --- a/tests/PHPStan/Analyser/nsrt/array-slice.php +++ b/tests/PHPStan/Analyser/nsrt/array-slice.php @@ -36,6 +36,22 @@ public function normalArrays(array $arr): void /** @var array $arr */ assertType('array', array_slice($arr, 1, 2)); assertType('array', array_slice($arr, 1, 2, true)); + + /** @var non-empty-array $arr */ + assertType('array{}', array_slice($arr, 0, 0)); + assertType('array{}', array_slice($arr, 0, 0, true)); + + /** @var non-empty-array $arr */ + assertType('array', array_slice($arr, 0, 1)); + assertType('array', array_slice($arr, 0, 1, true)); + + /** @var list $arr */ + assertType('list', array_slice($arr, 0, 1)); + assertType('list', array_slice($arr, 0, 1, true)); + + /** @var non-empty-list $arr */ + assertType('non-empty-list', array_slice($arr, 0, 1)); + assertType('non-empty-list', array_slice($arr, 0, 1, true)); } public function constantArrays(array $arr): void @@ -48,6 +64,14 @@ public function constantArrays(array $arr): void /** @var array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr */ assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2)); assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr, 1, 2, true)); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{}', array_slice($arr, -1, -1)); + assertType('array{}', array_slice($arr, -1, -1, true)); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{}', array_slice($arr, -1, -2)); + assertType('array{}', array_slice($arr, -1, -2, true)); } public function constantArraysWithOptionalKeys(array $arr): void diff --git a/tests/PHPStan/Analyser/nsrt/bug-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php index 918b56e624..f90994ee89 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5017.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5017.php @@ -15,7 +15,7 @@ public function doFoo() assertType('non-empty-array<0|1|2|3|4, 0|1|2|3|4>', $items); $batch = array_splice($items, 0, 2); assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items); - assertType('non-empty-list<0|1|2|3|4>', $batch); + assertType('list<0|1|2|3|4>', $batch); } } @@ -28,7 +28,7 @@ public function doBar($items) assertType('non-empty-array', $items); $batch = array_splice($items, 0, 2); assertType('array', $items); - assertType('non-empty-array', $batch); + assertType('array', $batch); } }