Skip to content

Commit 8425b3c

Browse files
committed
Add Type::spliceArray(), improve splice_array() array type narrowing
1 parent 38bc819 commit 8425b3c

21 files changed

+436
-22
lines changed

Diff for: src/Analyser/NodeScopeResolver.php

+9-8
Original file line numberDiff line numberDiff line change
@@ -2668,19 +2668,20 @@ static function (): void {
26682668
if (
26692669
$functionReflection !== null
26702670
&& $functionReflection->getName() === 'array_splice'
2671-
&& count($expr->getArgs()) >= 1
2671+
&& count($expr->getArgs()) >= 2
26722672
) {
26732673
$arrayArg = $expr->getArgs()[0]->value;
26742674
$arrayArgType = $scope->getType($arrayArg);
2675-
$valueType = $arrayArgType->getIterableValueType();
2676-
if (count($expr->getArgs()) >= 4) {
2677-
$replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray();
2678-
$valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType());
2679-
}
2675+
$arrayArgNativeType = $scope->getNativeType($arrayArg);
2676+
2677+
$offsetType = $scope->getType($expr->getArgs()[1]->value);
2678+
$lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType();
2679+
$replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []);
2680+
26802681
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
26812682
$arrayArg,
2682-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2683-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2683+
$arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
2684+
$arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
26842685
);
26852686
}
26862687

Diff for: src/Type/Accessory/AccessoryArrayListType.php

+5
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();

Diff for: src/Type/Accessory/HasOffsetType.php

+9
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();

Diff for: src/Type/Accessory/HasOffsetValueType.php

+9
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();

Diff for: src/Type/Accessory/NonEmptyArrayType.php

+12
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
226226
return new MixedType();
227227
}
228228

229+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
230+
{
231+
if (
232+
(new ConstantIntegerType(0))->isSuperTypeOf($lengthType)->yes()
233+
|| $replacementType->isIterableAtLeastOnce()->yes()
234+
) {
235+
return $this;
236+
}
237+
238+
return new MixedType();
239+
}
240+
229241
public function isIterable(): TrinaryLogic
230242
{
231243
return TrinaryLogic::createYes();

Diff for: src/Type/Accessory/OversizedArrayType.php

+5
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();

Diff for: src/Type/ArrayType.php

+17
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,23 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
449449
return $this;
450450
}
451451

452+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
453+
{
454+
$replacementArrayType = $replacementType->toArray();
455+
$replacementArrayTypeIsIterableAtLeastOnce = $replacementArrayType->isIterableAtLeastOnce();
456+
457+
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes() && $lengthType->isNull()->yes() && $replacementArrayTypeIsIterableAtLeastOnce->no()) {
458+
return new ConstantArrayType([], []);
459+
}
460+
461+
$arrayType = new self($this->getIterableKeyType(), TypeCombinator::union($this->getIterableValueType(), $replacementArrayType->getIterableValueType()));
462+
if ($replacementArrayTypeIsIterableAtLeastOnce->yes()) {
463+
$arrayType = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
464+
}
465+
466+
return $arrayType;
467+
}
468+
452469
public function isCallable(): TrinaryLogic
453470
{
454471
return TrinaryLogic::createMaybe()->and($this->itemType->isString());

Diff for: src/Type/Constant/ConstantArrayType.php

+115-1
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
945945
}
946946

947947
if ($keyTypesCount + $offset <= 0) {
948-
// A negative offset cannot reach left outside the array
948+
// A negative offset cannot reach left outside the array twice
949949
$offset = 0;
950950
}
951951

@@ -1006,6 +1006,120 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
10061006
return $builder->getArray();
10071007
}
10081008

1009+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
1010+
{
1011+
$keyTypesCount = count($this->keyTypes);
1012+
1013+
$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : null;
1014+
1015+
if ($lengthType instanceof ConstantIntegerType) {
1016+
$length = $lengthType->getValue();
1017+
} elseif ($lengthType->isNull()->yes()) {
1018+
$length = $keyTypesCount;
1019+
} else {
1020+
$length = null;
1021+
}
1022+
1023+
if ($offset === null || $length === null) {
1024+
return $this->degradeToGeneralArray()
1025+
->spliceArray($offsetType, $lengthType, $replacementType);
1026+
}
1027+
1028+
if ($keyTypesCount + $offset <= 0) {
1029+
// A negative offset cannot reach left outside the array twice
1030+
$offset = 0;
1031+
}
1032+
1033+
if ($offset < 0) {
1034+
/*
1035+
* Transforms the problem with the negative offset in one with a positive offset using array reversion.
1036+
* The reason is belows handling of optional keys which works only from left to right.
1037+
*
1038+
* e.g.
1039+
* array{a: 0, b: 1, c: 2, d: 3, e: 4}
1040+
* with offset -4, length 2 and replacement array{17, 19} (which would be spliced to array{a: 0, 0: 17, 1: 19, d: 3, e: 4})
1041+
*
1042+
* is transformed via reversion to
1043+
*
1044+
* array{e: 4, d: 3, c: 2, b: 1, a: 0}
1045+
* with offset 2 and length 2 (which will be spliced to array{e: 4, d: 3, 1: 19, 0: 17, a: 0} and then reversed again)
1046+
*/
1047+
$offset *= -1;
1048+
$reversedLength = min($length, $offset);
1049+
$reversedOffset = $offset - $reversedLength;
1050+
return $this->reverseArray(TrinaryLogic::createNo())
1051+
->spliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $replacementType->reverseArray(TrinaryLogic::createYes()))
1052+
->reverseArray(TrinaryLogic::createNo());
1053+
}
1054+
1055+
if ($length < 0) {
1056+
$length = $keyTypesCount - $offset - $length * -1;
1057+
}
1058+
1059+
$removeKeysCount = 0;
1060+
$optionalKeysIgnored = 0;
1061+
$optionalKeysBeforeReplacement = 0;
1062+
$builder = ConstantArrayTypeBuilder::createEmpty();
1063+
for ($i = 0;; $i++) {
1064+
$isOptional = $this->isOptionalKey($i);
1065+
1066+
if ($i < $offset && $isOptional) {
1067+
$optionalKeysIgnored++;
1068+
$optionalKeysBeforeReplacement++;
1069+
}
1070+
1071+
if ($i === $offset + $optionalKeysBeforeReplacement) {
1072+
// When the offset is reached we have to a) put the replacement array in and b) remove $length elements
1073+
$removeKeysCount = $length;
1074+
$optionalKeysIgnored += $optionalKeysBeforeReplacement;
1075+
1076+
$replacementArrayType = $replacementType->toArray();
1077+
$constantArrays = $replacementArrayType->getConstantArrays();
1078+
if (count($constantArrays) === 1) {
1079+
$valuesArray = $constantArrays[0]->getValuesArray();
1080+
for ($j = 0, $jMax = count($valuesArray->keyTypes); $j < $jMax; $j++) {
1081+
$builder->setOffsetValueType(null, $valuesArray->valueTypes[$j], $valuesArray->isOptionalKey($j));
1082+
}
1083+
} else {
1084+
$builder->degradeToGeneralArray();
1085+
$builder->setOffsetValueType(null, $replacementArrayType->getIterableValueType(), true);
1086+
}
1087+
}
1088+
1089+
if (!isset($this->keyTypes[$i])) {
1090+
break;
1091+
}
1092+
1093+
if ($removeKeysCount > 0) {
1094+
$removeKeysCount--;
1095+
1096+
if ($optionalKeysIgnored === 0) {
1097+
if ($isOptional) {
1098+
$optionalKeysIgnored++;
1099+
}
1100+
1101+
continue;
1102+
} else {
1103+
$optionalKeysIgnored++;
1104+
$isOptional = true;
1105+
}
1106+
}
1107+
1108+
if (!$isOptional && $optionalKeysIgnored > 0) {
1109+
$optionalKeysIgnored--;
1110+
$isOptional = true;
1111+
}
1112+
1113+
$builder->setOffsetValueType(
1114+
$this->keyTypes[$i]->isInteger()->no() ? $this->keyTypes[$i] : null,
1115+
$this->valueTypes[$i],
1116+
$isOptional,
1117+
);
1118+
}
1119+
1120+
return $builder->getArray();
1121+
}
1122+
10091123
public function isIterableAtLeastOnce(): TrinaryLogic
10101124
{
10111125
$keysCount = count($this->keyTypes);

Diff for: src/Type/IntersectionType.php

+5
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,11 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
899899
return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
900900
}
901901

902+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
903+
{
904+
return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
905+
}
906+
902907
public function getEnumCases(): array
903908
{
904909
$compare = [];

Diff for: src/Type/MixedType.php

+9
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) {

Diff for: src/Type/NeverType.php

+5
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();

Diff for: src/Type/StaticType.php

+5
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();

Diff for: src/Type/Traits/LateResolvableTypeTrait.php

+5
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();

Diff for: src/Type/Traits/MaybeArrayTypeTrait.php

+5
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
}

Diff for: src/Type/Traits/NonArrayTypeTrait.php

+5
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
}

Diff for: src/Type/Type.php

+2
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
*/

Diff for: src/Type/UnionType.php

+5
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(

0 commit comments

Comments
 (0)