diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index f9856ee4dbd..8cec286eaf3 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -595,25 +595,46 @@ public function removeFirst(): Type public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self { - if (count($this->keyTypes) === 0) { + $keyTypesCount = count($this->keyTypes); + if ($keyTypesCount === 0) { return $this; } - $keyTypes = array_slice($this->keyTypes, $offset, $limit); - $valueTypes = array_slice($this->valueTypes, $offset, $limit); + $optionalKeysCount = count($this->optionalKeys); + if ($offset < 0) { + $offset = max(0, $keyTypesCount + $offset - $optionalKeysCount); + } + if ($limit !== null && $limit < 0) { + $limit = max(0, $keyTypesCount + $limit - $offset - $optionalKeysCount); + } + if ($limit === 0) { + return new self([], []); + } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = 0; - foreach ($keyTypes as $keyType) { - if (!$keyType instanceof ConstantIntegerType) { - continue; + $builder = ConstantArrayTypeBuilder::createEmpty(); + $sliceIndex = 0; + foreach ($this->keyTypes as $i => $keyType) { + $isOptional = $this->isOptionalKey($i); + $nextSliceIndex = $isOptional ? $sliceIndex : $sliceIndex + 1; + $reachedLimit = $limit !== null && $nextSliceIndex - $offset >= $limit; + + if ($sliceIndex >= $offset) { + // The last value gets optional if the limit was reached and optional values have been added before + $currentOffsetIsOptional = $isOptional || ($reachedLimit && $i > $sliceIndex); + $builder->setOffsetValueType($keyType, $this->valueTypes[$i], $currentOffsetIsOptional); } - /** @var int|float $nextAutoIndex */ - $nextAutoIndex = max($nextAutoIndex, $keyType->getValue() + 1); + $sliceIndex = $nextSliceIndex; + + if ($reachedLimit) { + break; + } } - $slice = new self($keyTypes, $valueTypes, (int) $nextAutoIndex, []); + $slice = $builder->getArray(); + if (!$slice instanceof ConstantArrayType) { + throw new ShouldNotHappenException(); + } return $preserveKeys ? $slice : $slice->reindex(); } diff --git a/tests/PHPStan/Analyser/data/array-slice.php b/tests/PHPStan/Analyser/data/array-slice.php index 3a863ad22c6..5c0a45b555b 100644 --- a/tests/PHPStan/Analyser/data/array-slice.php +++ b/tests/PHPStan/Analyser/data/array-slice.php @@ -23,22 +23,46 @@ public function fromMixed($arr): void assertType('array', array_slice($arr, 1, 2)); } - /** - * @param array $arr1 - * @param array $arr2 - * @param array{17: 'foo', b: 'bar', 19: 'baz'} $arr3 - * @param array{17: 'foo', 19: 'bar', 21: 'baz'}|array{foo: 17, bar: 19, baz: 21} $arr4 - */ - public function preserveTypes(array $arr1, array $arr2, array $arr3, array $arr4): void + public function preserveTypes(array $arr): void { - assertType('array', array_slice($arr1, 1, 2)); - assertType('array', array_slice($arr1, 1, 2, true)); - assertType('array', array_slice($arr2, 1, 2)); - assertType('array', array_slice($arr2, 1, 2, true)); - assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr3, 1, 2)); - assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr3, 1, 2, true)); - assertType('array{\'bar\', \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr4, 1, 2)); - assertType('array{19: \'bar\', 21: \'baz\'}|array{bar: 19, baz: 21}', array_slice($arr4, 1, 2, true)); + /** @var array $arr */ + assertType('array', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2, true)); + + /** @var array $arr */ + assertType('array', array_slice($arr, 1, 2)); + assertType('array', array_slice($arr, 1, 2, true)); + + /** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */ + assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr, 1, 2)); + assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr, 1, 2, true)); + assertType('array{0: \'foo\', b: \'bar\'}', array_slice($arr, 0, -1)); + assertType('array{17: \'foo\', b: \'bar\'}', array_slice($arr, 0, -1, true)); + assertType('array{b: \'bar\', 0: \'baz\'}', array_slice($arr, -2, null)); + assertType('array{b: \'bar\', 19: \'baz\'}', array_slice($arr, -2, null, true)); + + /** @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{foo?: 17, bar: 19} $arr */ + assertType('array{}', array_slice($arr, 0, 0)); + assertType('array{}', array_slice($arr, -1, 0)); + assertType('array{}', array_slice($arr, 1, 0)); + assertType('array{}', array_slice($arr, 1, 1)); + assertType('array{foo?: 17, bar?: 19}', array_slice($arr, 0, 1)); + assertType('array{foo?: 17, bar: 19}', array_slice($arr, 0)); + assertType('array{foo?: 17, bar: 19}', array_slice($arr, -99)); + assertType('array{foo?: 17, bar: 19}', array_slice($arr, 0, 99)); + + /** @var array{foo: 17, bar?: 19, baz: 21, foobar?: 23, barfoo: 25} $arr */ + assertType('array{bar?: 19, baz: 21, foobar?: 23, barfoo?: 25}', array_slice($arr, 1, 2)); + assertType('array{foo: 17, bar?: 19, baz?: 21}', array_slice($arr, 0, 2)); + assertType('array{foo: 17}', array_slice($arr, 0, 1)); + assertType('array{bar?: 19, baz?: 21}', array_slice($arr, 1, 1)); + assertType('array{foobar?: 23, barfoo?: 25}', array_slice($arr, 2, 1)); + assertType('array{foo: 17, bar?: 19, baz?: 21}', array_slice($arr, 0, -1)); + assertType('array{bar?: 19, baz: 21, foobar?: 23, barfoo: 25}', array_slice($arr, -2, null)); } }