diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index f6550a5845e..288caefdc63 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -521,13 +521,35 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen return new ConstantArrayType([], []); } + $existingArrayKeyType = $this->getIterableKeyType(); + $keyType = TypeTraverser::map($existingArrayKeyType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + if ($type->isInteger()->yes()) { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + + return $type; + }); + $arrayType = new self( - TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()), + TypeCombinator::union($keyType, $replacementArrayType->getKeysArray()->getIterableKeyType()), TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()), ); + $accessories = []; if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) { - $arrayType = new IntersectionType([$arrayType, new NonEmptyArrayType()]); + $accessories[] = new NonEmptyArrayType(); + } + if ($existingArrayKeyType->isInteger()->yes()) { + $accessories[] = new AccessoryArrayListType(); + } + if (count($accessories) > 0) { + $accessories[] = $arrayType; + + return new IntersectionType($accessories); } return $arrayType; diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 522c587fbdb..019aa62eeb3 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1499,7 +1499,7 @@ public static function intersect(Type ...$types): Type return $types[0]; } - return new IntersectionType(array_values($types)); + return new IntersectionType($types); } public static function removeFalsey(Type $type): Type diff --git a/tests/PHPStan/Analyser/nsrt/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php index 92385d0786f..9e524d32125 100644 --- a/tests/PHPStan/Analyser/nsrt/array_splice.php +++ b/tests/PHPStan/Analyser/nsrt/array_splice.php @@ -21,52 +21,52 @@ function insertViaArraySplice(array $arr): void { $brr = $arr; $extract = array_splice($brr, 0, 0, 1); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, [1]); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, ''); - assertType('non-empty-array', $brr); + assertType('non-empty-list<\'\'|int>', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, ['']); - assertType('non-empty-array', $brr); + assertType('non-empty-list<\'\'|int>', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, null); - assertType('array', $brr); + assertType('list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, [null]); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, new Foo()); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, [new \stdClass()]); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, false); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; $extract = array_splice($brr, 0, 0, [false]); - assertType('non-empty-array', $brr); + assertType('non-empty-list', $brr); assertType('array{}', $extract); $brr = $arr; @@ -323,25 +323,25 @@ function offsets(array $arr): void { if (array_key_exists(1, $arr)) { $extract = array_splice($arr, 0, 1, 'hello'); - assertType('non-empty-array', $arr); + assertType('non-empty-array<(int<0, max>|string), mixed>', $arr); assertType('array', $extract); } if (array_key_exists(1, $arr)) { $extract = array_splice($arr, 0, 0, 'hello'); - assertType('non-empty-array&hasOffset(1)', $arr); + assertType('non-empty-array<(int<0, max>|string), mixed>&hasOffset(1)', $arr); assertType('array{}', $extract); } if (array_key_exists(1, $arr) && $arr[1] === 'foo') { $extract = array_splice($arr, 0, 1, 'hello'); - assertType('non-empty-array', $arr); + assertType('non-empty-array<(int<0, max>|string), mixed>', $arr); assertType('array', $extract); } if (array_key_exists(1, $arr) && $arr[1] === 'foo') { $extract = array_splice($arr, 0, 0, 'hello'); - assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr); + assertType('non-empty-array<(int<0, max>|string), mixed>&hasOffsetValue(1, \'foo\')', $arr); assertType('array{}', $extract); } } @@ -384,3 +384,11 @@ function lists(array $arr): void assertType('list', $arr); assertType('list', $extract); } + +/** @param array $arr */ +function more(array $arr): void +{ + $extract = array_splice($arr, 0, 1, [17 => 'foo', 18 => 'bar']); + assertType('non-empty-array<0|1|string, string>', $arr); + assertType('array', $extract); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-14037.php b/tests/PHPStan/Analyser/nsrt/bug-14037.php new file mode 100644 index 00000000000..c64d1cc7e06 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14037.php @@ -0,0 +1,21 @@ + $a + */ +function splice(array $a): void { + array_splice($a, 0, 0); + assertType("array<'a'|int<0, max>, mixed>", $a); +} + +/** + * @param array|'a', mixed> $a + */ +function splice2(array $a): void { + array_splice($a, 0, 0); + assertType("array<'a'|int<0, max>, mixed>", $a); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-4743.php b/tests/PHPStan/Analyser/nsrt/bug-4743.php index a5aeab22237..3546a12d8c3 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-4743.php +++ b/tests/PHPStan/Analyser/nsrt/bug-4743.php @@ -28,7 +28,7 @@ public function splice(int $offset, int $length): void { $newNodes = array_splice($this->nodes, $offset, $length); - assertType('array', $this->nodes); + assertType('array<(int<0, max>|string), T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $this->nodes); assertType('array', $newNodes); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-5017.php b/tests/PHPStan/Analyser/nsrt/bug-5017.php index c4e7cfebaa6..aa3abb30359 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-5017.php +++ b/tests/PHPStan/Analyser/nsrt/bug-5017.php @@ -27,7 +27,7 @@ public function doBar($items) while ($items) { assertType('non-empty-array', $items); $batch = array_splice($items, 0, 2); - assertType('array', $items); + assertType('array<(int<0, max>|string), int>', $items); assertType('array', $batch); } } @@ -49,7 +49,7 @@ public function doBar3(array $ints, array $strings) { $removed = array_splice($ints, 0, 2, $strings); assertType('array', $removed); - assertType('array', $ints); + assertType('array<(int<0, max>|string), int|string>', $ints); assertType('array', $strings); } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 59ed255a09e..1e2b616b9ac 100644 --- a/tests/PHPStan/Type/ArrayTypeTest.php +++ b/tests/PHPStan/Type/ArrayTypeTest.php @@ -301,4 +301,86 @@ public function testHasOffsetValueType( ); } + public static function dataSpliceArray(): array + { + return [ + [ + new ArrayType(new UnionType([ + new ConstantIntegerType(10), + new ConstantIntegerType(20), + new ConstantIntegerType(30), + new ConstantStringType('a'), + ]), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + "array<'a'|int<0, max>, mixed>", + ], + [ + new ArrayType(new UnionType([ + new IntegerType(), + new ConstantStringType('a'), + ]), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + "array<'a'|int<0, max>, mixed>", + ], + [ + new ArrayType(new IntegerType(), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + 'list', + ], + [ + new ArrayType(new StringType(), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + 'array', + ], + [ + new ArrayType(new MixedType(), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType([], []), + 'array<(int<0, max>|string), mixed>', + ], + [ + new ArrayType(new StringType(), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ConstantArrayType( + [new ConstantStringType('key')], + [new ConstantStringType('value')], + ), + 'non-empty-array<0|string, mixed>', + ], + [ + new ArrayType(new StringType(), new MixedType()), + new ConstantIntegerType(0), + new ConstantIntegerType(0), + new ArrayType(new MixedType(), new MixedType()), + 'array|string, mixed>', + ], + ]; + } + + #[DataProvider('dataSpliceArray')] + public function testSpliceArray( + ArrayType $type, + Type $offsetType, + Type $lengthType, + Type $replacementType, + string $expectedType, + ): void + { + $actualResult = $type->spliceArray($offsetType, $lengthType, $replacementType); + $this->assertSame( + $expectedType, + $actualResult->describe(VerbosityLevel::precise()), + ); + } + }