From 2f48d3197ba51541b86f20a7f14b3d9fedfbbe09 Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Sun, 1 Feb 2026 23:22:21 +0900 Subject: [PATCH 1/2] Fix array types after `array_splice()` --- src/Type/ArrayType.php | 22 +++++- tests/PHPStan/Analyser/nsrt/array_splice.php | 28 +++---- tests/PHPStan/Analyser/nsrt/bug-14037.php | 13 ++++ tests/PHPStan/Analyser/nsrt/bug-4743.php | 2 +- tests/PHPStan/Analyser/nsrt/bug-5017.php | 4 +- tests/PHPStan/Type/ArrayTypeTest.php | 82 ++++++++++++++++++++ 6 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14037.php diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index f6550a5845..52b45ef5e9 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -521,13 +521,31 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen return new ConstantArrayType([], []); } + $isIntegerKey = $this->keyType->isInteger(); + if ($isIntegerKey->yes()) { + $keyType = IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } elseif ($isIntegerKey->maybe()) { + $keyType = TypeCombinator::union( + TypeCombinator::remove($this->getIterableKeyType(), new IntegerType()), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ); + } else { + $keyType = TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()); + } $arrayType = new self( - TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()), + $keyType, TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()), ); + $accessories = []; if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) { - $arrayType = new IntersectionType([$arrayType, new NonEmptyArrayType()]); + $accessories[] = new NonEmptyArrayType(); + } + if ($isIntegerKey->yes()) { + $accessories[] = new AccessoryArrayListType(); + } + if ($accessories !== []) { + $arrayType = new IntersectionType([$arrayType, ...$accessories]); } return $arrayType; diff --git a/tests/PHPStan/Analyser/nsrt/array_splice.php b/tests/PHPStan/Analyser/nsrt/array_splice.php index 92385d0786..0ebd1abea2 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|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|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|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|string, mixed>&hasOffsetValue(1, \'foo\')', $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 0000000000..dec3b683c6 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14037.php @@ -0,0 +1,13 @@ + $a + */ +function splice(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 a5aeab2223..84a41da854 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|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 c4e7cfebaa..d78096b8ab 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|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|string, int|string>', $ints); assertType('array', $strings); } diff --git a/tests/PHPStan/Type/ArrayTypeTest.php b/tests/PHPStan/Type/ArrayTypeTest.php index 59ed255a09..9879a90509 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|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()), + ); + } + } From 83efe86dc60b4d07daa2e36c1139c36873593ec6 Mon Sep 17 00:00:00 2001 From: Takuya Aramaki Date: Sun, 1 Feb 2026 23:28:55 +0900 Subject: [PATCH 2/2] Remove a redundant `array_values()` detected by PHPStan --- src/Type/TypeCombinator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index aa5f933532..5cf038d55c 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -1497,7 +1497,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