Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2ba876f

Browse files
committedMay 2, 2025·
Add Type::spliceArray(), improve splice_array() array type narrowing
1 parent d404f01 commit 2ba876f

22 files changed

+597
-32
lines changed
 

‎phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ parameters:
969969
-
970970
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
971971
identifier: phpstanApi.instanceofType
972-
count: 5
972+
count: 6
973973
path: src/Type/Constant/ConstantArrayType.php
974974

975975
-

‎src/Analyser/NodeScopeResolver.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2670,19 +2670,20 @@ static function (): void {
26702670
if (
26712671
$functionReflection !== null
26722672
&& $functionReflection->getName() === 'array_splice'
2673-
&& count($expr->getArgs()) >= 1
2673+
&& count($expr->getArgs()) >= 2
26742674
) {
26752675
$arrayArg = $expr->getArgs()[0]->value;
26762676
$arrayArgType = $scope->getType($arrayArg);
2677-
$valueType = $arrayArgType->getIterableValueType();
2678-
if (count($expr->getArgs()) >= 4) {
2679-
$replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray();
2680-
$valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType());
2681-
}
2677+
$arrayArgNativeType = $scope->getNativeType($arrayArg);
2678+
2679+
$offsetType = $scope->getType($expr->getArgs()[1]->value);
2680+
$lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType();
2681+
$replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []);
2682+
26822683
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
26832684
$arrayArg,
2684-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2685-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2685+
$arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
2686+
$arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
26862687
);
26872688
}
26882689

‎src/Type/Accessory/AccessoryArrayListType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
248248
return new MixedType();
249249
}
250250

251+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
252+
{
253+
return $this;
254+
}
255+
251256
public function isIterable(): TrinaryLogic
252257
{
253258
return TrinaryLogic::createYes();

‎src/Type/Accessory/HasOffsetType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
214214
return new MixedType();
215215
}
216216

217+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
218+
{
219+
if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) {
220+
return $this;
221+
}
222+
223+
return new MixedType();
224+
}
225+
217226
public function isIterableAtLeastOnce(): TrinaryLogic
218227
{
219228
return TrinaryLogic::createYes();

‎src/Type/Accessory/HasOffsetValueType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
274274
return new MixedType();
275275
}
276276

277+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
278+
{
279+
if ((new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()) {
280+
return $this;
281+
}
282+
283+
return new MixedType();
284+
}
285+
277286
public function isIterableAtLeastOnce(): TrinaryLogic
278287
{
279288
return TrinaryLogic::createYes();

‎src/Type/Accessory/NonEmptyArrayType.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,18 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
223223
return new MixedType();
224224
}
225225

226+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
227+
{
228+
if (
229+
(new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()
230+
|| $replacementType->toArray()->isIterableAtLeastOnce()->yes()
231+
) {
232+
return $this;
233+
}
234+
235+
return new MixedType();
236+
}
237+
226238
public function isIterable(): TrinaryLogic
227239
{
228240
return TrinaryLogic::createYes();

‎src/Type/Accessory/OversizedArrayType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
214214
return $this;
215215
}
216216

217+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
218+
{
219+
return $this;
220+
}
221+
217222
public function isIterable(): TrinaryLogic
218223
{
219224
return TrinaryLogic::createYes();

‎src/Type/ArrayType.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,27 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
453453
return $this;
454454
}
455455

456+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
457+
{
458+
$replacementArrayType = $replacementType->toArray();
459+
$replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce();
460+
461+
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes() && $replacementArrayTypeIsIterableAtLeastOnce->no()) {
462+
return new ConstantArrayType([], []);
463+
}
464+
465+
$arrayType = new self(
466+
TypeCombinator::union($this->getIterableKeyType(), $replacementArrayType->getKeysArray()->getIterableKeyType()),
467+
TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()),
468+
);
469+
470+
if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) {
471+
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
472+
}
473+
474+
return $arrayType;
475+
}
476+
456477
public function isCallable(): TrinaryLogic
457478
{
458479
return TrinaryLogic::createMaybe()->and($this->itemType->isString());

‎src/Type/Constant/ConstantArrayType.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,108 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
10161016
return $builder->getArray();
10171017
}
10181018

1019+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1020+
{
1021+
$keyTypesCount = count($this->keyTypes);
1022+
1023+
$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1024+
1025+
if ($lengthType instanceof ConstantIntegerType) {
1026+
$length = $lengthType->getValue();
1027+
} elseif ($lengthType->isNull()->yes()) {
1028+
$length = $keyTypesCount;
1029+
} else {
1030+
$length = null;
1031+
}
1032+
1033+
if ($offset === null || $length === null) {
1034+
return $this->degradeToGeneralArray()
1035+
->spliceArray($offsetType, $lengthType, $replacementType);
1036+
}
1037+
1038+
if ($keyTypesCount + $offset <= 0) {
1039+
// A negative offset cannot reach left outside the array twice
1040+
$offset = 0;
1041+
}
1042+
1043+
if ($keyTypesCount + $length <= 0) {
1044+
// A negative length cannot reach left outside the array twice
1045+
$length = 0;
1046+
}
1047+
1048+
$offsetWasNegative = false;
1049+
if ($offset < 0) {
1050+
$offsetWasNegative = true;
1051+
$offset = $keyTypesCount + $offset;
1052+
}
1053+
1054+
if ($length < 0) {
1055+
$length = $keyTypesCount - $offset + $length;
1056+
}
1057+
1058+
$extractType = $this->sliceArray($offsetType, $lengthType, TrinaryLogic::createYes());
1059+
1060+
$types = [];
1061+
foreach ($replacementType->toArray()->getArrays() as $replacementArrayType) {
1062+
$removeKeysCount = 0;
1063+
$optionalKeysBeforeReplacement = 0;
1064+
1065+
$builder = ConstantArrayTypeBuilder::createEmpty();
1066+
for ($i = 0;; $i++) {
1067+
$isOptional = $this->isOptionalKey($i);
1068+
1069+
if (!$offsetWasNegative && $i < $offset && $isOptional) {
1070+
$optionalKeysBeforeReplacement++;
1071+
}
1072+
1073+
if ($i === $offset + $optionalKeysBeforeReplacement) {
1074+
// When the offset is reached we have to a) put the replacement array in and b) remove $length elements
1075+
$removeKeysCount = $length;
1076+
1077+
if ($replacementArrayType instanceof self) {
1078+
$valuesArray = $replacementArrayType->getValuesArray();
1079+
for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) {
1080+
$builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j));
1081+
}
1082+
} else {
1083+
$builder->degradeToGeneralArray();
1084+
$builder->setOffsetValueType($replacementArrayType->getValuesArray()->getIterableKeyType(), $replacementArrayType->getIterableValueType(), true);
1085+
}
1086+
}
1087+
1088+
if (!isset($this->keyTypes[$i])) {
1089+
break;
1090+
}
1091+
1092+
if ($removeKeysCount > 0) {
1093+
$extractTypeHasOffsetValueType = $extractType->hasOffsetValueType($this->keyTypes[$i]);
1094+
1095+
if (
1096+
(!$isOptional && $extractTypeHasOffsetValueType->yes())
1097+
|| ($isOptional && $extractTypeHasOffsetValueType->maybe())
1098+
) {
1099+
$removeKeysCount--;
1100+
continue;
1101+
}
1102+
}
1103+
1104+
if (!$isOptional && $extractType->hasOffsetValueType($this->keyTypes[$i])->maybe()) {
1105+
$isOptional = true;
1106+
}
1107+
1108+
$builder->setOffsetValueType(
1109+
$this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null,
1110+
$this->valueTypes[$i],
1111+
$isOptional,
1112+
);
1113+
}
1114+
1115+
$types[] = $builder->getArray();
1116+
}
1117+
1118+
return TypeCombinator::union(...$types);
1119+
}
1120+
10191121
public function isIterableAtLeastOnce(): TrinaryLogic
10201122
{
10211123
$keysCount = count($this->keyTypes);

‎src/Type/IntersectionType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
910910
return $result;
911911
}
912912

913+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
914+
{
915+
return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
916+
}
917+
913918
public function getEnumCases(): array
914919
{
915920
$compare = [];

‎src/Type/MixedType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,15 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
287287
return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
288288
}
289289

290+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
291+
{
292+
if ($this->isArray()->no()) {
293+
return new ErrorType();
294+
}
295+
296+
return new ArrayType(new MixedType($this->isExplicitMixed), new MixedType($this->isExplicitMixed));
297+
}
298+
290299
public function isCallable(): TrinaryLogic
291300
{
292301
if ($this->subtractedType !== null) {

‎src/Type/NeverType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
333333
return new NeverType();
334334
}
335335

336+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
337+
{
338+
return new NeverType();
339+
}
340+
336341
public function isCallable(): TrinaryLogic
337342
{
338343
return TrinaryLogic::createNo();

‎src/Type/StaticType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
456456
return $this->getStaticObjectType()->sliceArray($offsetType, $lengthType, $preserveKeys);
457457
}
458458

459+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
460+
{
461+
return $this->getStaticObjectType()->spliceArray($offsetType, $lengthType, $replacementType);
462+
}
463+
459464
public function isCallable(): TrinaryLogic
460465
{
461466
return $this->getStaticObjectType()->isCallable();

‎src/Type/Traits/LateResolvableTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
308308
return $this->resolve()->sliceArray($offsetType, $lengthType, $preserveKeys);
309309
}
310310

311+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
312+
{
313+
return $this->resolve()->spliceArray($offsetType, $lengthType, $replacementType);
314+
}
315+
311316
public function isCallable(): TrinaryLogic
312317
{
313318
return $this->resolve()->isCallable();

‎src/Type/Traits/MaybeArrayTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,9 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
9999
return new ErrorType();
100100
}
101101

102+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
103+
{
104+
return new ErrorType();
105+
}
106+
102107
}

‎src/Type/Traits/NonArrayTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,9 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
9999
return new ErrorType();
100100
}
101101

102+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
103+
{
104+
return new ErrorType();
105+
}
106+
102107
}

‎src/Type/Type.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ public function shuffleArray(): Type;
158158

159159
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type;
160160

161+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type;
162+
161163
/**
162164
* @return list<EnumCaseObjectType>
163165
*/

‎src/Type/UnionType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
790790
return $this->unionTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
791791
}
792792

793+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
794+
{
795+
return $this->unionTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
796+
}
797+
793798
public function getEnumCases(): array
794799
{
795800
return $this->pickFromTypes(

‎tests/PHPStan/Analyser/nsrt/array_splice.php

Lines changed: 344 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,42 +20,367 @@ final class Foo
2020
function insertViaArraySplice(array $arr): void
2121
{
2222
$brr = $arr;
23-
array_splice($brr, 0, 0, 1);
24-
assertType('array<int, int>', $brr);
23+
$extract = array_splice($brr, 0, 0, 1);
24+
assertType('non-empty-array<int, int>', $brr);
25+
assertType('array{}', $extract);
2526

2627
$brr = $arr;
27-
array_splice($brr, 0, 0, [1]);
28-
assertType('array<int, int>', $brr);
28+
$extract = array_splice($brr, 0, 0, [1]);
29+
assertType('non-empty-array<int, int>', $brr);
30+
assertType('array{}', $extract);
2931

3032
$brr = $arr;
31-
array_splice($brr, 0, 0, '');
32-
assertType('array<int, \'\'|int>', $brr);
33+
$extract = array_splice($brr, 0, 0, '');
34+
assertType('non-empty-array<int, \'\'|int>', $brr);
35+
assertType('array{}', $extract);
3336

3437
$brr = $arr;
35-
array_splice($brr, 0, 0, ['']);
36-
assertType('array<int, \'\'|int>', $brr);
38+
$extract = array_splice($brr, 0, 0, ['']);
39+
assertType('non-empty-array<int, \'\'|int>', $brr);
40+
assertType('array{}', $extract);
3741

3842
$brr = $arr;
39-
array_splice($brr, 0, 0, null);
43+
$extract = array_splice($brr, 0, 0, null);
4044
assertType('array<int, int>', $brr);
45+
assertType('array{}', $extract);
46+
47+
$brr = $arr;
48+
$extract = array_splice($brr, 0, 0, [null]);
49+
assertType('non-empty-array<int, int|null>', $brr);
50+
assertType('array{}', $extract);
4151

4252
$brr = $arr;
43-
array_splice($brr, 0, 0, [null]);
44-
assertType('array<int, int|null>', $brr);
53+
$extract = array_splice($brr, 0, 0, new Foo());
54+
assertType('non-empty-array<int, bool|int|string>', $brr);
55+
assertType('array{}', $extract);
4556

4657
$brr = $arr;
47-
array_splice($brr, 0, 0, new Foo());
48-
assertType('array<int, bool|int|string>', $brr);
58+
$extract = array_splice($brr, 0, 0, [new \stdClass()]);
59+
assertType('non-empty-array<int, int|stdClass>', $brr);
60+
assertType('array{}', $extract);
4961

5062
$brr = $arr;
51-
array_splice($brr, 0, 0, [new \stdClass()]);
52-
assertType('array<int, int|stdClass>', $brr);
63+
$extract = array_splice($brr, 0, 0, false);
64+
assertType('non-empty-array<int, int|false>', $brr);
65+
assertType('array{}', $extract);
5366

5467
$brr = $arr;
55-
array_splice($brr, 0, 0, false);
56-
assertType('array<int, int|false>', $brr);
68+
$extract = array_splice($brr, 0, 0, [false]);
69+
assertType('non-empty-array<int, int|false>', $brr);
70+
assertType('array{}', $extract);
5771

5872
$brr = $arr;
59-
array_splice($brr, 0, 0, [false]);
60-
assertType('array<int, int|false>', $brr);
73+
$extract = array_splice($brr, 0);
74+
assertType('array{}', $brr);
75+
assertType('list<int>', $extract);
76+
}
77+
78+
function constantArrays(array $arr, array $arr2): void
79+
{
80+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
81+
$arr;
82+
$extract = array_splice($arr, 0, 1, ['hello']);
83+
assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr);
84+
assertType('array{\'foo\'}', $extract);
85+
86+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
87+
$arr;
88+
$extract = array_splice($arr, 1, 2, ['hello']);
89+
assertType('array{\'foo\', \'hello\'}', $arr);
90+
assertType('array{b: \'bar\', 0: \'baz\'}', $extract);
91+
92+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
93+
$arr;
94+
$extract = array_splice($arr, 0, -1, ['hello']);
95+
assertType('array{\'hello\', \'baz\'}', $arr);
96+
assertType('array{0: \'foo\', b: \'bar\'}', $extract);
97+
98+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
99+
$arr;
100+
$extract = array_splice($arr, 0, -2, ['hello']);
101+
assertType('array{0: \'hello\', b: \'bar\', 1: \'baz\'}', $arr);
102+
assertType('array{\'foo\'}', $extract);
103+
104+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
105+
$arr;
106+
$extract = array_splice($arr, -1, -1, ['hello']);
107+
assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\', 2: \'baz\'}', $arr);
108+
assertType('array{}', $extract);
109+
110+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
111+
$arr;
112+
$extract = array_splice($arr, -2, -2, ['hello']);
113+
assertType('array{0: \'foo\', 1: \'hello\', b: \'bar\', 2: \'baz\'}', $arr);
114+
assertType('array{}', $extract);
115+
116+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
117+
$arr;
118+
$extract = array_splice($arr, 99, 0, ['hello']);
119+
assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $arr);
120+
assertType('array{}', $extract);
121+
122+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
123+
$arr;
124+
$extract = array_splice($arr, 1, 99, ['hello']);
125+
assertType('array{\'foo\', \'hello\'}', $arr);
126+
assertType('array{b: \'bar\', 0: \'baz\'}', $extract);
127+
128+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
129+
$arr;
130+
$extract = array_splice($arr, -99, 99, ['hello']);
131+
assertType('array{\'hello\'}', $arr);
132+
assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract);
133+
134+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
135+
$arr;
136+
$extract = array_splice($arr, 0, -99, ['hello']);
137+
assertType('array{0: \'hello\', 1: \'foo\', b: \'bar\', 2: \'baz\'}', $arr);
138+
assertType('array{}', $extract);
139+
140+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
141+
$arr;
142+
$extract = array_splice($arr, -2, 1, ['hello']);
143+
assertType('array{\'foo\', \'hello\', \'baz\'}', $arr);
144+
assertType('array{b: \'bar\'}', $extract);
145+
146+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
147+
$arr;
148+
$extract = array_splice($arr, -1, 1, ['hello']);
149+
assertType('array{0: \'foo\', b: \'bar\', 1: \'hello\'}', $arr);
150+
assertType('array{\'baz\'}', $extract);
151+
152+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
153+
$arr;
154+
$extract = array_splice($arr, 0, null, ['hello']);
155+
assertType('array{\'hello\'}', $arr);
156+
assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract);
157+
158+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
159+
$arr;
160+
$extract = array_splice($arr, 0);
161+
assertType('array{}', $arr);
162+
assertType('array{0: \'foo\', b: \'bar\', 1: \'baz\'}', $extract);
163+
164+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
165+
/** @var array<\stdClass> $arr2 */
166+
$arr;
167+
$extract = array_splice($arr, 1, 1, $arr2);
168+
assertType('non-empty-array<int<0, max>, \'baz\'|\'foo\'|stdClass>', $arr);
169+
assertType('array{b: \'bar\'}', $extract);
170+
171+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
172+
/** @var array<\stdClass> $arr2 */
173+
$arr;
174+
$extract = array_splice($arr, 0, 1, $arr2);
175+
assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|stdClass>', $arr);
176+
assertType('array{\'foo\'}', $extract);
177+
178+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
179+
/** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int} $arr2 */
180+
$arr;
181+
$extract = array_splice($arr, 0, 1, $arr2);
182+
assertType('array{0: \'x\'|\'z\', 1: \'y\'|int, 2: \'baz\'|int, b: \'bar\', 3?: \'baz\'}', $arr);
183+
assertType('array{\'foo\'}', $extract);
184+
185+
/** @var array{17: 'foo', b: 'bar', 19: 'baz'} $arr */
186+
/** @var array{x: 'x', y?: 'y', 3: 66}|array{z: 'z', 5?: 77, 4: int}|array<object|null> $arr2 */
187+
$arr;
188+
$extract = array_splice($arr, 0, 1, $arr2);
189+
assertType('non-empty-array<\'b\'|int<0, max>, \'bar\'|\'baz\'|\'x\'|\'y\'|\'z\'|int|object|null>', $arr);
190+
assertType('array{\'foo\'}', $extract);
191+
}
192+
193+
function constantArraysWithOptionalKeys(array $arr): void
194+
{
195+
/**
196+
* @see https://3v4l.org/2UJ3u
197+
* @var array{a?: 0, b: 1, c: 2} $arr
198+
*/
199+
$arr;
200+
$extract = array_splice($arr, 0, 1, ['hello']);
201+
assertType('array{0: \'hello\', b?: 1, c: 2}', $arr);
202+
assertType('array{a?: 0, b?: 1}', $extract);
203+
204+
/**
205+
* @see https://3v4l.org/Aq4l6
206+
* @var array{a?: 0, b: 1, c: 2} $arr
207+
*/
208+
$arr;
209+
$extract = array_splice($arr, 1, 1, ['hello']);
210+
assertType('array{a?: 0, b?: 1, 0: \'hello\', c?: 2}', $arr);
211+
assertType('array{b?: 1, c?: 2}', $extract);
212+
213+
/**
214+
* @see https://3v4l.org/GBMps
215+
* @var array{a?: 0, b: 1, c: 2} $arr
216+
*/
217+
$arr;
218+
$extract = array_splice($arr, -1, 0, ['hello']);
219+
assertType('array{a?: 0, b: 1, 0: \'hello\', c: 2}', $arr);
220+
assertType('array{}', $extract);
221+
222+
/**
223+
* @see https://3v4l.org/dQVgY
224+
* @var array{a?: 0, b: 1, c: 2} $arr
225+
*/
226+
$arr;
227+
$extract = array_splice($arr, 0, -1, ['hello']);
228+
assertType('array{0: \'hello\', c: 2}', $arr);
229+
assertType('array{a?: 0, b: 1}', $extract);
230+
231+
/**
232+
* @see https://3v4l.org/5XWRC
233+
* @var array{a: 0, b?: 1, c: 2} $arr
234+
*/
235+
$arr;
236+
$extract = array_splice($arr, 0, 1, ['hello']);
237+
assertType('array{0: \'hello\', b?: 1, c: 2}', $arr);
238+
assertType('array{a: 0}', $extract);
239+
240+
/**
241+
* @see https://3v4l.org/QXZre
242+
* @var array{a: 0, b?: 1, c: 2} $arr
243+
*/
244+
$arr;
245+
$extract = array_splice($arr, 1, 1, ['hello']);
246+
assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr);
247+
assertType('array{b?: 1, c?: 2}', $extract);
248+
249+
/**
250+
* @see https://3v4l.org/4JvMu
251+
* @var array{a: 0, b?: 1, c: 2} $arr
252+
*/
253+
$arr;
254+
$extract = array_splice($arr, -1, 0, ['hello']);
255+
assertType('array{a: 0, b?: 1, 0: \'hello\', c: 2}', $arr);
256+
assertType('array{}', $extract);
257+
258+
/**
259+
* @see https://3v4l.org/srHon
260+
* @var array{a: 0, b?: 1, c: 2} $arr
261+
*/
262+
$arr;
263+
$extract = array_splice($arr, 0, -1, ['hello']);
264+
assertType('array{0: \'hello\', c: 2}', $arr);
265+
assertType('array{a: 0, b?: 1}', $extract);
266+
267+
/**
268+
* @see https://3v4l.org/d0b0c
269+
* @var array{a: 0, b: 1, c?: 2} $arr
270+
*/
271+
$arr;
272+
$extract = array_splice($arr, 0, 1, ['hello']);
273+
assertType('array{0: \'hello\', b: 1, c?: 2}', $arr);
274+
assertType('array{a: 0}', $extract);
275+
276+
/**
277+
* @see https://3v4l.org/OPfIf
278+
* @var array{a: 0, b: 1, c?: 2} $arr
279+
*/
280+
$arr;
281+
$extract = array_splice($arr, 1, 1, ['hello']);
282+
assertType('array{a: 0, 0: \'hello\', c?: 2}', $arr);
283+
assertType('array{b: 1}', $extract);
284+
285+
/**
286+
* @see https://3v4l.org/b9R9E
287+
* @var array{a: 0, b: 1, c?: 2} $arr
288+
*/
289+
$arr;
290+
$extract = array_splice($arr, -1, 0, ['hello']);
291+
assertType('array{a: 0, b: 1, 0: \'hello\', c?: 2}', $arr);
292+
assertType('array{}', $extract);
293+
294+
/**
295+
* @see https://3v4l.org/0lFX6
296+
* @var array{a: 0, b: 1, c?: 2} $arr
297+
*/
298+
$arr;
299+
$extract = array_splice($arr, 0, -1, ['hello']);
300+
assertType('array{0: \'hello\', b?: 1, c?: 2}', $arr);
301+
assertType('array{a: 0, b?: 1}', $extract);
302+
303+
/**
304+
* @see https://3v4l.org/PLHYv
305+
* @var array{a: 0, b?: 1, c?: 2, d: 3} $arr
306+
*/
307+
$arr;
308+
$extract = array_splice($arr, 1, 2, ['hello']);
309+
assertType('array{a: 0, 0: \'hello\', d?: 3}', $arr);
310+
assertType('array{b?: 1, c?: 2, d?: 3}', $extract);
311+
312+
/**
313+
* @see https://3v4l.org/Li5bj
314+
* @var array{a: 0, b?: 1, c?: 2, d: 3} $arr
315+
*/
316+
$arr;
317+
$extract = array_splice($arr, -2, 2, ['hello']);
318+
assertType('array{a?: 0, b?: 1, 0: \'hello\'}', $arr);
319+
assertType('array{a?: 0, b?: 1, c?: 2, d: 3}', $extract);
320+
}
321+
322+
function offsets(array $arr): void
323+
{
324+
if (array_key_exists(1, $arr)) {
325+
$extract = array_splice($arr, 0, 1, 'hello');
326+
assertType('non-empty-array', $arr);
327+
assertType('array', $extract);
328+
}
329+
330+
if (array_key_exists(1, $arr)) {
331+
$extract = array_splice($arr, 0, 0, 'hello');
332+
assertType('non-empty-array&hasOffset(1)', $arr);
333+
assertType('array{}', $extract);
334+
}
335+
336+
if (array_key_exists(1, $arr) && $arr[1] === 'foo') {
337+
$extract = array_splice($arr, 0, 1, 'hello');
338+
assertType('non-empty-array', $arr);
339+
assertType('array', $extract);
340+
}
341+
342+
if (array_key_exists(1, $arr) && $arr[1] === 'foo') {
343+
$extract = array_splice($arr, 0, 0, 'hello');
344+
assertType('non-empty-array&hasOffsetValue(1, \'foo\')', $arr);
345+
assertType('array{}', $extract);
346+
}
347+
}
348+
349+
function lists(array $arr): void
350+
{
351+
/** @var list<string> $arr */
352+
$arr;
353+
$extract = array_splice($arr, 0, 1, 'hello');
354+
assertType('non-empty-list<string>', $arr);
355+
assertType('list<string>', $extract);
356+
357+
/** @var non-empty-list<string> $arr */
358+
$arr;
359+
$extract = array_splice($arr, 0, 1);
360+
assertType('list<string>', $arr);
361+
assertType('non-empty-list<string>', $extract);
362+
363+
/** @var list<string> $arr */
364+
$arr;
365+
$extract = array_splice($arr, 0, 0, 'hello');
366+
assertType('non-empty-list<string>', $arr);
367+
assertType('array{}', $extract);
368+
369+
/** @var list<string> $arr */
370+
$arr;
371+
$extract = array_splice($arr, 0, null, 'hello');
372+
assertType('non-empty-list<string>', $arr);
373+
assertType('list<string>', $extract);
374+
375+
/** @var list<string> $arr */
376+
$arr;
377+
$extract = array_splice($arr, 0, null);
378+
assertType('array{}', $arr);
379+
assertType('list<string>', $extract);
380+
381+
/** @var list<string> $arr */
382+
$arr;
383+
$extract = array_splice($arr, 0, 1);
384+
assertType('list<string>', $arr);
385+
assertType('list<string>', $extract);
61386
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11917;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @return list<string>
9+
*/
10+
function generateList(string $name): array
11+
{
12+
$a = ['a', 'b', 'c', $name];
13+
assertType('array{\'a\', \'b\', \'c\', string}', $a);
14+
$b = ['d', 'e'];
15+
assertType('array{\'d\', \'e\'}', $b);
16+
17+
array_splice($a, 2, 0, $b);
18+
assertType('array{\'a\', \'b\', \'d\', \'e\', \'c\', string}', $a);
19+
20+
return $a;
21+
}
22+
23+
var_dump(generateList('John'));

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ public function doFoo()
1212
$items = [0, 1, 2, 3, 4];
1313

1414
while ($items) {
15-
assertType('non-empty-array<0|1|2|3|4, 0|1|2|3|4>', $items);
15+
assertType('non-empty-list<int<min, 4>>', $items);
1616
$batch = array_splice($items, 0, 2);
17-
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items);
18-
assertType('list<0|1|2|3|4>', $batch);
17+
assertType('list<int<min, 4>>', $items);
18+
assertType('non-empty-list<int<min, 4>>', $batch);
1919
}
2020
}
2121

@@ -37,7 +37,7 @@ public function doBar2()
3737
$items = [0, 1, 2, 3, 4];
3838
assertType('array{0, 1, 2, 3, 4}', $items);
3939
$batch = array_splice($items, 0, 2);
40-
assertType('array<0|1|2|3|4, 0|1|2|3|4>', $items);
40+
assertType('array{2, 3, 4}', $items);
4141
assertType('array{0, 1}', $batch);
4242
}
4343

‎tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,13 @@ public function testBug11301(): void
345345
]);
346346
}
347347

348+
public function testBug11917(): void
349+
{
350+
$this->checkExplicitMixed = true;
351+
$this->checkNullables = true;
352+
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-11917.php'], []);
353+
}
354+
348355
public function testBug12274(): void
349356
{
350357
$this->checkExplicitMixed = true;

0 commit comments

Comments
 (0)
Please sign in to comment.