Skip to content

Commit 7a6b985

Browse files
Fix phpstan/phpstan#14037: array_splice resets int keys of the input array (phpstan#5139)
Co-authored-by: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent 8a020e0 commit 7a6b985

File tree

7 files changed

+153
-20
lines changed

7 files changed

+153
-20
lines changed

src/Type/ArrayType.php

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,13 +521,35 @@ public function spliceArray(Type $offsetType, Type $lengthType, Type $replacemen
521521
return new ConstantArrayType([], []);
522522
}
523523

524+
$existingArrayKeyType = $this->getIterableKeyType();
525+
$keyType = TypeTraverser::map($existingArrayKeyType, static function (Type $type, callable $traverse): Type {
526+
if ($type instanceof UnionType) {
527+
return $traverse($type);
528+
}
529+
530+
if ($type->isInteger()->yes()) {
531+
return IntegerRangeType::createAllGreaterThanOrEqualTo(0);
532+
}
533+
534+
return $type;
535+
});
536+
524537
$arrayType = new self(
525-
TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()),
538+
TypeCombinator::union($keyType, $replacementArrayType->getKeysArray()->getIterableKeyType()),
526539
TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()),
527540
);
528541

542+
$accessories = [];
529543
if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) {
530-
$arrayType = new IntersectionType([$arrayType, new NonEmptyArrayType()]);
544+
$accessories[] = new NonEmptyArrayType();
545+
}
546+
if ($existingArrayKeyType->isInteger()->yes()) {
547+
$accessories[] = new AccessoryArrayListType();
548+
}
549+
if (count($accessories) > 0) {
550+
$accessories[] = $arrayType;
551+
552+
return new IntersectionType($accessories);
531553
}
532554

533555
return $arrayType;

src/Type/TypeCombinator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@ public static function intersect(Type ...$types): Type
14991499
return $types[0];
15001500
}
15011501

1502-
return new IntersectionType(array_values($types));
1502+
return new IntersectionType($types);
15031503
}
15041504

15051505
public static function removeFalsey(Type $type): Type

tests/PHPStan/Analyser/nsrt/array_splice.php

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,52 +21,52 @@ function insertViaArraySplice(array $arr): void
2121
{
2222
$brr = $arr;
2323
$extract = array_splice($brr, 0, 0, 1);
24-
assertType('non-empty-array<int, int>', $brr);
24+
assertType('non-empty-list<int>', $brr);
2525
assertType('array{}', $extract);
2626

2727
$brr = $arr;
2828
$extract = array_splice($brr, 0, 0, [1]);
29-
assertType('non-empty-array<int, int>', $brr);
29+
assertType('non-empty-list<int>', $brr);
3030
assertType('array{}', $extract);
3131

3232
$brr = $arr;
3333
$extract = array_splice($brr, 0, 0, '');
34-
assertType('non-empty-array<int, \'\'|int>', $brr);
34+
assertType('non-empty-list<\'\'|int>', $brr);
3535
assertType('array{}', $extract);
3636

3737
$brr = $arr;
3838
$extract = array_splice($brr, 0, 0, ['']);
39-
assertType('non-empty-array<int, \'\'|int>', $brr);
39+
assertType('non-empty-list<\'\'|int>', $brr);
4040
assertType('array{}', $extract);
4141

4242
$brr = $arr;
4343
$extract = array_splice($brr, 0, 0, null);
44-
assertType('array<int, int>', $brr);
44+
assertType('list<int>', $brr);
4545
assertType('array{}', $extract);
4646

4747
$brr = $arr;
4848
$extract = array_splice($brr, 0, 0, [null]);
49-
assertType('non-empty-array<int, int|null>', $brr);
49+
assertType('non-empty-list<int|null>', $brr);
5050
assertType('array{}', $extract);
5151

5252
$brr = $arr;
5353
$extract = array_splice($brr, 0, 0, new Foo());
54-
assertType('non-empty-array<int, bool|int|string>', $brr);
54+
assertType('non-empty-list<bool|int|string>', $brr);
5555
assertType('array{}', $extract);
5656

5757
$brr = $arr;
5858
$extract = array_splice($brr, 0, 0, [new \stdClass()]);
59-
assertType('non-empty-array<int, int|stdClass>', $brr);
59+
assertType('non-empty-list<int|stdClass>', $brr);
6060
assertType('array{}', $extract);
6161

6262
$brr = $arr;
6363
$extract = array_splice($brr, 0, 0, false);
64-
assertType('non-empty-array<int, int|false>', $brr);
64+
assertType('non-empty-list<int|false>', $brr);
6565
assertType('array{}', $extract);
6666

6767
$brr = $arr;
6868
$extract = array_splice($brr, 0, 0, [false]);
69-
assertType('non-empty-array<int, int|false>', $brr);
69+
assertType('non-empty-list<int|false>', $brr);
7070
assertType('array{}', $extract);
7171

7272
$brr = $arr;
@@ -323,25 +323,25 @@ function offsets(array $arr): void
323323
{
324324
if (array_key_exists(1, $arr)) {
325325
$extract = array_splice($arr, 0, 1, 'hello');
326-
assertType('non-empty-array', $arr);
326+
assertType('non-empty-array<(int<0, max>|string), mixed>', $arr);
327327
assertType('array', $extract);
328328
}
329329

330330
if (array_key_exists(1, $arr)) {
331331
$extract = array_splice($arr, 0, 0, 'hello');
332-
assertType('non-empty-array&hasOffset(1)', $arr);
332+
assertType('non-empty-array<(int<0, max>|string), mixed>&hasOffset(1)', $arr);
333333
assertType('array{}', $extract);
334334
}
335335

336336
if (array_key_exists(1, $arr) && $arr[1] === 'foo') {
337337
$extract = array_splice($arr, 0, 1, 'hello');
338-
assertType('non-empty-array', $arr);
338+
assertType('non-empty-array<(int<0, max>|string), mixed>', $arr);
339339
assertType('array', $extract);
340340
}
341341

342342
if (array_key_exists(1, $arr) && $arr[1] === 'foo') {
343343
$extract = array_splice($arr, 0, 0, 'hello');
344-
assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr);
344+
assertType('non-empty-array<(int<0, max>|string), mixed>&hasOffsetValue(1, \'foo\')', $arr);
345345
assertType('array{}', $extract);
346346
}
347347
}
@@ -384,3 +384,11 @@ function lists(array $arr): void
384384
assertType('list<string>', $arr);
385385
assertType('list<string>', $extract);
386386
}
387+
388+
/** @param array<string, string> $arr */
389+
function more(array $arr): void
390+
{
391+
$extract = array_splice($arr, 0, 1, [17 => 'foo', 18 => 'bar']);
392+
assertType('non-empty-array<0|1|string, string>', $arr);
393+
assertType('array<string, string>', $extract);
394+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug14037;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param array<10|20|30|'a', mixed> $a
9+
*/
10+
function splice(array $a): void {
11+
array_splice($a, 0, 0);
12+
assertType("array<'a'|int<0, max>, mixed>", $a);
13+
}
14+
15+
/**
16+
* @param array<int<10, 30>|'a', mixed> $a
17+
*/
18+
function splice2(array $a): void {
19+
array_splice($a, 0, 0);
20+
assertType("array<'a'|int<0, max>, mixed>", $a);
21+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function splice(int $offset, int $length): void
2828
{
2929
$newNodes = array_splice($this->nodes, $offset, $length);
3030

31-
assertType('array<T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $this->nodes);
31+
assertType('array<(int<0, max>|string), T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $this->nodes);
3232
assertType('array<T of Bug4743\\Node (class Bug4743\\NodeList, argument)>', $newNodes);
3333
}
3434
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function doBar($items)
2727
while ($items) {
2828
assertType('non-empty-array<int>', $items);
2929
$batch = array_splice($items, 0, 2);
30-
assertType('array<int>', $items);
30+
assertType('array<(int<0, max>|string), int>', $items);
3131
assertType('array<int>', $batch);
3232
}
3333
}
@@ -49,7 +49,7 @@ public function doBar3(array $ints, array $strings)
4949
{
5050
$removed = array_splice($ints, 0, 2, $strings);
5151
assertType('array<int>', $removed);
52-
assertType('array<int|string>', $ints);
52+
assertType('array<(int<0, max>|string), int|string>', $ints);
5353
assertType('array<string>', $strings);
5454
}
5555

tests/PHPStan/Type/ArrayTypeTest.php

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,4 +301,86 @@ public function testHasOffsetValueType(
301301
);
302302
}
303303

304+
public static function dataSpliceArray(): array
305+
{
306+
return [
307+
[
308+
new ArrayType(new UnionType([
309+
new ConstantIntegerType(10),
310+
new ConstantIntegerType(20),
311+
new ConstantIntegerType(30),
312+
new ConstantStringType('a'),
313+
]), new MixedType()),
314+
new ConstantIntegerType(0),
315+
new ConstantIntegerType(0),
316+
new ConstantArrayType([], []),
317+
"array<'a'|int<0, max>, mixed>",
318+
],
319+
[
320+
new ArrayType(new UnionType([
321+
new IntegerType(),
322+
new ConstantStringType('a'),
323+
]), new MixedType()),
324+
new ConstantIntegerType(0),
325+
new ConstantIntegerType(0),
326+
new ConstantArrayType([], []),
327+
"array<'a'|int<0, max>, mixed>",
328+
],
329+
[
330+
new ArrayType(new IntegerType(), new MixedType()),
331+
new ConstantIntegerType(0),
332+
new ConstantIntegerType(0),
333+
new ConstantArrayType([], []),
334+
'list',
335+
],
336+
[
337+
new ArrayType(new StringType(), new MixedType()),
338+
new ConstantIntegerType(0),
339+
new ConstantIntegerType(0),
340+
new ConstantArrayType([], []),
341+
'array<string, mixed>',
342+
],
343+
[
344+
new ArrayType(new MixedType(), new MixedType()),
345+
new ConstantIntegerType(0),
346+
new ConstantIntegerType(0),
347+
new ConstantArrayType([], []),
348+
'array<(int<0, max>|string), mixed>',
349+
],
350+
[
351+
new ArrayType(new StringType(), new MixedType()),
352+
new ConstantIntegerType(0),
353+
new ConstantIntegerType(0),
354+
new ConstantArrayType(
355+
[new ConstantStringType('key')],
356+
[new ConstantStringType('value')],
357+
),
358+
'non-empty-array<0|string, mixed>',
359+
],
360+
[
361+
new ArrayType(new StringType(), new MixedType()),
362+
new ConstantIntegerType(0),
363+
new ConstantIntegerType(0),
364+
new ArrayType(new MixedType(), new MixedType()),
365+
'array<int<0, max>|string, mixed>',
366+
],
367+
];
368+
}
369+
370+
#[DataProvider('dataSpliceArray')]
371+
public function testSpliceArray(
372+
ArrayType $type,
373+
Type $offsetType,
374+
Type $lengthType,
375+
Type $replacementType,
376+
string $expectedType,
377+
): void
378+
{
379+
$actualResult = $type->spliceArray($offsetType, $lengthType, $replacementType);
380+
$this->assertSame(
381+
$expectedType,
382+
$actualResult->describe(VerbosityLevel::precise()),
383+
);
384+
}
385+
304386
}

0 commit comments

Comments
 (0)