Skip to content

Commit acd2a44

Browse files
authored
Add Type::spliceArray(), improve splice_array() array type narrowing
1 parent 9979dce commit acd2a44

22 files changed

+607
-40
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ parameters:
948948
-
949949
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantArrayType is error\-prone and deprecated\. Use Type\:\:getConstantArrays\(\) instead\.$#'
950950
identifier: phpstanApi.instanceofType
951-
count: 5
951+
count: 6
952952
path: src/Type/Constant/ConstantArrayType.php
953953

954954
-

src/Analyser/NodeScopeResolver.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2685,19 +2685,20 @@ static function (): void {
26852685
if (
26862686
$functionReflection !== null
26872687
&& $functionReflection->getName() === 'array_splice'
2688-
&& count($expr->getArgs()) >= 1
2688+
&& count($expr->getArgs()) >= 2
26892689
) {
26902690
$arrayArg = $expr->getArgs()[0]->value;
26912691
$arrayArgType = $scope->getType($arrayArg);
2692-
$valueType = $arrayArgType->getIterableValueType();
2693-
if (count($expr->getArgs()) >= 4) {
2694-
$replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray();
2695-
$valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType());
2696-
}
2692+
$arrayArgNativeType = $scope->getNativeType($arrayArg);
2693+
2694+
$offsetType = $scope->getType($expr->getArgs()[1]->value);
2695+
$lengthType = isset($expr->getArgs()[2]) ? $scope->getType($expr->getArgs()[2]->value) : new NullType();
2696+
$replacementType = isset($expr->getArgs()[3]) ? $scope->getType($expr->getArgs()[3]->value) : new ConstantArrayType([], []);
2697+
26972698
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
26982699
$arrayArg,
2699-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2700-
new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2700+
$arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
2701+
$arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
27012702
);
27022703
}
27032704

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: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -919,10 +919,7 @@ public function shiftArray(): Type
919919

920920
public function shuffleArray(): Type
921921
{
922-
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this->getValuesArray());
923-
$builder->degradeToGeneralArray();
924-
925-
return $builder->getArray();
922+
return $this->getValuesArray()->degradeToGeneralArray();
926923
}
927924

928925
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
@@ -943,10 +940,7 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
943940
}
944941

945942
if ($offset === null || $length === null) {
946-
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
947-
$builder->degradeToGeneralArray();
948-
949-
return $builder->getArray()
943+
return $this->degradeToGeneralArray()
950944
->sliceArray($offsetType, $lengthType, $preserveKeys);
951945
}
952946

@@ -1028,6 +1022,108 @@ public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $pre
10281022
return $builder->getArray();
10291023
}
10301024

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

1367+
private function degradeToGeneralArray(): Type
1368+
{
1369+
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
1370+
$builder->degradeToGeneralArray();
1371+
1372+
return $builder->getArray();
1373+
}
1374+
12711375
public function getKeysArray(): self
12721376
{
12731377
return $this->getKeysOrValuesArray($this->keyTypes);

src/Type/IntersectionType.php

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

916+
public function spliceArray(Type $offsetType, Type $lengthType, Type $replacementType): Type
917+
{
918+
return $this->intersectTypes(static fn (Type $type): Type => $type->spliceArray($offsetType, $lengthType, $replacementType));
919+
}
920+
916921
public function getEnumCases(): array
917922
{
918923
$compare = [];

0 commit comments

Comments
 (0)