Skip to content

Commit 768e1c3

Browse files
committed
Add Type::sliceArray()
1 parent ff1f737 commit 768e1c3

20 files changed

+244
-123
lines changed

Diff for: phpstan-baseline.neon

+1-1
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ parameters:
794794

795795
-
796796
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#"
797-
count: 8
797+
count: 10
798798
path: src/Type/Constant/ConstantArrayType.php
799799

800800
-

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

+13
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,19 @@ public function shuffleArray(): Type
247247
return $this;
248248
}
249249

250+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
251+
{
252+
if ($preserveKeys->no()) {
253+
return $this;
254+
}
255+
256+
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) {
257+
return $this;
258+
}
259+
260+
return new MixedType();
261+
}
262+
250263
public function isIterable(): TrinaryLogic
251264
{
252265
return TrinaryLogic::createYes();

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

+16
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Type\Constant\ConstantIntegerType;
1313
use PHPStan\Type\Constant\ConstantStringType;
1414
use PHPStan\Type\ErrorType;
15+
use PHPStan\Type\IntegerRangeType;
1516
use PHPStan\Type\IntersectionType;
1617
use PHPStan\Type\MixedType;
1718
use PHPStan\Type\ObjectWithoutClassType;
@@ -25,6 +26,7 @@
2526
use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
2627
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
2728
use PHPStan\Type\Type;
29+
use PHPStan\Type\TypeCombinator;
2830
use PHPStan\Type\UnionType;
2931
use PHPStan\Type\VerbosityLevel;
3032
use function sprintf;
@@ -208,6 +210,20 @@ public function shuffleArray(): Type
208210
return new NonEmptyArrayType();
209211
}
210212

213+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
214+
{
215+
if (
216+
$this->offsetType->isSuperTypeOf($offsetType)->yes()
217+
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
218+
) {
219+
return $preserveKeys->yes()
220+
? TypeCombinator::intersect($this, new NonEmptyArrayType())
221+
: new NonEmptyArrayType();
222+
}
223+
224+
return new MixedType();
225+
}
226+
211227
public function isIterableAtLeastOnce(): TrinaryLogic
212228
{
213229
return TrinaryLogic::createYes();

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

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPStan\Type\Constant\ConstantStringType;
1515
use PHPStan\Type\ConstantScalarType;
1616
use PHPStan\Type\ErrorType;
17+
use PHPStan\Type\IntegerRangeType;
1718
use PHPStan\Type\IntersectionType;
1819
use PHPStan\Type\MixedType;
1920
use PHPStan\Type\ObjectWithoutClassType;
@@ -27,6 +28,7 @@
2728
use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
2829
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
2930
use PHPStan\Type\Type;
31+
use PHPStan\Type\TypeCombinator;
3032
use PHPStan\Type\UnionType;
3133
use PHPStan\Type\VerbosityLevel;
3234
use function sprintf;
@@ -264,6 +266,20 @@ public function shuffleArray(): Type
264266
return new NonEmptyArrayType();
265267
}
266268

269+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
270+
{
271+
if (
272+
$this->offsetType->isSuperTypeOf($offsetType)->yes()
273+
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
274+
) {
275+
return $preserveKeys->yes()
276+
? TypeCombinator::intersect($this, new NonEmptyArrayType())
277+
: new NonEmptyArrayType();
278+
}
279+
280+
return new MixedType();
281+
}
282+
267283
public function isIterableAtLeastOnce(): TrinaryLogic
268284
{
269285
return TrinaryLogic::createYes();

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

+12
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,18 @@ public function shuffleArray(): Type
224224
return $this;
225225
}
226226

227+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
228+
{
229+
if (
230+
(new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()
231+
&& ($lengthType->isNull()->yes() || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($lengthType)->yes())
232+
) {
233+
return $this;
234+
}
235+
236+
return new MixedType();
237+
}
238+
227239
public function isIterable(): TrinaryLogic
228240
{
229241
return TrinaryLogic::createYes();

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

+5
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,11 @@ public function shuffleArray(): Type
220220
return $this;
221221
}
222222

223+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
224+
{
225+
return $this;
226+
}
227+
223228
public function isIterable(): TrinaryLogic
224229
{
225230
return TrinaryLogic::createYes();

Diff for: src/Type/ArrayType.php

+5
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,11 @@ public function shuffleArray(): Type
591591
return AccessoryArrayListType::intersectWith(new self(new IntegerType(), $this->itemType));
592592
}
593593

594+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
595+
{
596+
return $this;
597+
}
598+
594599
public function isCallable(): TrinaryLogic
595600
{
596601
return TrinaryLogic::createMaybe()->and($this->itemType->isString());

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

+97-78
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
use PHPStan\Type\CompoundType;
3232
use PHPStan\Type\ConstantScalarType;
3333
use PHPStan\Type\ConstantType;
34+
use PHPStan\Type\ConstantTypeHelper;
3435
use PHPStan\Type\ErrorType;
3536
use PHPStan\Type\GeneralizePrecision;
3637
use PHPStan\Type\Generic\TemplateTypeMap;
@@ -39,6 +40,7 @@
3940
use PHPStan\Type\IntersectionType;
4041
use PHPStan\Type\MixedType;
4142
use PHPStan\Type\NeverType;
43+
use PHPStan\Type\NullType;
4244
use PHPStan\Type\Type;
4345
use PHPStan\Type\TypeCombinator;
4446
use PHPStan\Type\UnionType;
@@ -812,7 +814,7 @@ public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
812814

813815
$keyTypesCount = count($this->keyTypes);
814816
for ($i = 0; $i < $keyTypesCount; $i += $length) {
815-
$chunk = $this->slice($i, $length, true);
817+
$chunk = $this->sliceArray(new ConstantIntegerType($i), new ConstantIntegerType($length), TrinaryLogic::createYes());
816818
$builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
817819
}
818820

@@ -882,7 +884,7 @@ public function popArray(): Type
882884
return $this->removeLastElements(1);
883885
}
884886

885-
private function reverseConstantArray(TrinaryLogic $preserveKeys): self
887+
public function reverseArray(TrinaryLogic $preserveKeys): Type
886888
{
887889
$keyTypesReversed = array_reverse($this->keyTypes, true);
888890
$keyTypes = array_values($keyTypesReversed);
@@ -894,11 +896,6 @@ private function reverseConstantArray(TrinaryLogic $preserveKeys): self
894896
return $preserveKeys->yes() ? $reversed : $reversed->reindex();
895897
}
896898

897-
public function reverseArray(TrinaryLogic $preserveKeys): Type
898-
{
899-
return $this->reverseConstantArray($preserveKeys);
900-
}
901-
902899
public function searchArray(Type $needleType): Type
903900
{
904901
$matches = [];
@@ -957,6 +954,85 @@ public function shuffleArray(): Type
957954
return $generalizedArray;
958955
}
959956

957+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
958+
{
959+
$keyTypesCount = count($this->keyTypes);
960+
if ($keyTypesCount === 0) {
961+
return $this;
962+
}
963+
964+
$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0;
965+
$length = $lengthType instanceof ConstantIntegerType ? $lengthType->getValue() : $keyTypesCount;
966+
967+
if ($length < 0) {
968+
// Negative lengths prevent access to the most right n elements
969+
return $this->removeLastElements($length * -1)
970+
->sliceArray($offsetType, new NullType(), $preserveKeys);
971+
}
972+
973+
if ($keyTypesCount + $offset <= 0) {
974+
// A negative offset cannot reach left outside the array
975+
$offset = 0;
976+
}
977+
978+
if ($offset < 0) {
979+
/*
980+
* Transforms the problem with the negative offset in one with a positive offset using array reversion.
981+
* The reason is belows handling of optional keys which works only from left to right.
982+
*
983+
* e.g.
984+
* array{a: 0, b: 1, c: 2, d: 3, e: 4}
985+
* with offset -4 and length 2 (which would be sliced to array{b: 1, c: 2})
986+
*
987+
* is transformed via reversion to
988+
*
989+
* array{e: 4, d: 3, c: 2, b: 1, a: 0}
990+
* with offset 2 and length 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
991+
*/
992+
$offset *= -1;
993+
$reversedLength = min($length, $offset);
994+
$reversedOffset = $offset - $reversedLength;
995+
return $this->reverseArray(TrinaryLogic::createYes())
996+
->sliceArray(new ConstantIntegerType($reversedOffset), new ConstantIntegerType($reversedLength), $preserveKeys)
997+
->reverseArray(TrinaryLogic::createYes());
998+
}
999+
1000+
if ($offset > 0) {
1001+
return $this->removeFirstElements($offset, false)
1002+
->sliceArray(new ConstantIntegerType(0), $lengthType, $preserveKeys);
1003+
}
1004+
1005+
$builder = ConstantArrayTypeBuilder::createEmpty();
1006+
1007+
$nonOptionalElementsCount = 0;
1008+
$hasOptional = false;
1009+
for ($i = 0; $nonOptionalElementsCount < $length && $i < $keyTypesCount; $i++) {
1010+
$isOptional = $this->isOptionalKey($i);
1011+
if (!$isOptional) {
1012+
$nonOptionalElementsCount++;
1013+
} else {
1014+
$hasOptional = true;
1015+
}
1016+
1017+
$isLastElement = $nonOptionalElementsCount >= $length || $i + 1 >= $keyTypesCount;
1018+
if ($isLastElement && $length < $keyTypesCount && $hasOptional) {
1019+
// If the slice is not full yet, but has at least one optional key
1020+
// the last non-optional element is going to be optional.
1021+
// Otherwise, it would not fit into the slice if previous non-optional keys are there.
1022+
$isOptional = true;
1023+
}
1024+
1025+
$builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i], $isOptional);
1026+
}
1027+
1028+
$slice = $builder->getArray();
1029+
if (!$slice instanceof self) {
1030+
throw new ShouldNotHappenException();
1031+
}
1032+
1033+
return $preserveKeys->yes() ? $slice : $slice->reindex();
1034+
}
1035+
9601036
public function isIterableAtLeastOnce(): TrinaryLogic
9611037
{
9621038
$keysCount = count($this->keyTypes);
@@ -1147,87 +1223,30 @@ private function removeFirstElements(int $length, bool $reindex = true): self
11471223
return $array;
11481224
}
11491225

1226+
/** @deprecated Use sliceArray() instead */
11501227
public function slice(int $offset, ?int $limit, bool $preserveKeys = false): self
11511228
{
1152-
$keyTypesCount = count($this->keyTypes);
1153-
if ($keyTypesCount === 0) {
1154-
return $this;
1155-
}
1156-
1157-
$limit ??= $keyTypesCount;
1158-
if ($limit < 0) {
1159-
// Negative limits prevent access to the most right n elements
1160-
return $this->removeLastElements($limit * -1)
1161-
->slice($offset, null, $preserveKeys);
1162-
}
1163-
1164-
if ($keyTypesCount + $offset <= 0) {
1165-
// A negative offset cannot reach left outside the array
1166-
$offset = 0;
1167-
}
1168-
1169-
if ($offset < 0) {
1170-
/*
1171-
* Transforms the problem with the negative offset in one with a positive offset using array reversion.
1172-
* The reason is belows handling of optional keys which works only from left to right.
1173-
*
1174-
* e.g.
1175-
* array{a: 0, b: 1, c: 2, d: 3, e: 4}
1176-
* with offset -4 and limit 2 (which would be sliced to array{b: 1, c: 2})
1177-
*
1178-
* is transformed via reversion to
1179-
*
1180-
* array{e: 4, d: 3, c: 2, b: 1, a: 0}
1181-
* with offset 2 and limit 2 (which will be sliced to array{c: 2, b: 1} and then reversed again)
1182-
*/
1183-
$offset *= -1;
1184-
$reversedLimit = min($limit, $offset);
1185-
$reversedOffset = $offset - $reversedLimit;
1186-
return $this->reverseConstantArray(TrinaryLogic::createYes())
1187-
->slice($reversedOffset, $reversedLimit, $preserveKeys)
1188-
->reverseConstantArray(TrinaryLogic::createYes());
1189-
}
1190-
1191-
if ($offset > 0) {
1192-
return $this->removeFirstElements($offset, false)
1193-
->slice(0, $limit, $preserveKeys);
1194-
}
1195-
1196-
$builder = ConstantArrayTypeBuilder::createEmpty();
1197-
1198-
$nonOptionalElementsCount = 0;
1199-
$hasOptional = false;
1200-
for ($i = 0; $nonOptionalElementsCount < $limit && $i < $keyTypesCount; $i++) {
1201-
$isOptional = $this->isOptionalKey($i);
1202-
if (!$isOptional) {
1203-
$nonOptionalElementsCount++;
1204-
} else {
1205-
$hasOptional = true;
1206-
}
1207-
1208-
$isLastElement = $nonOptionalElementsCount >= $limit || $i + 1 >= $keyTypesCount;
1209-
if ($isLastElement && $limit < $keyTypesCount && $hasOptional) {
1210-
// If the slice is not full yet, but has at least one optional key
1211-
// the last non-optional element is going to be optional.
1212-
// Otherwise, it would not fit into the slice if previous non-optional keys are there.
1213-
$isOptional = true;
1214-
}
1215-
1216-
$builder->setOffsetValueType($this->keyTypes[$i], $this->valueTypes[$i], $isOptional);
1217-
}
1218-
1219-
$slice = $builder->getArray();
1220-
if (!$slice instanceof self) {
1229+
$array = $this->sliceArray(
1230+
ConstantTypeHelper::getTypeFromValue($offset),
1231+
ConstantTypeHelper::getTypeFromValue($limit),
1232+
TrinaryLogic::createFromBoolean($preserveKeys),
1233+
);
1234+
if (!$array instanceof self) {
12211235
throw new ShouldNotHappenException();
12221236
}
12231237

1224-
return $preserveKeys ? $slice : $slice->reindex();
1238+
return $array;
12251239
}
12261240

12271241
/** @deprecated Use reverseArray() instead */
12281242
public function reverse(bool $preserveKeys = false): self
12291243
{
1230-
return $this->reverseConstantArray(TrinaryLogic::createFromBoolean($preserveKeys));
1244+
$array = $this->reverseArray(TrinaryLogic::createFromBoolean($preserveKeys));
1245+
if (!$array instanceof self) {
1246+
throw new ShouldNotHappenException();
1247+
}
1248+
1249+
return $array;
12311250
}
12321251

12331252
/**

Diff for: src/Type/IntersectionType.php

+5
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,11 @@ public function shuffleArray(): Type
780780
return $this->intersectTypes(static fn (Type $type): Type => $type->shuffleArray());
781781
}
782782

783+
public function sliceArray(Type $offsetType, Type $lengthType, TrinaryLogic $preserveKeys): Type
784+
{
785+
return $this->intersectTypes(static fn (Type $type): Type => $type->sliceArray($offsetType, $lengthType, $preserveKeys));
786+
}
787+
783788
public function getEnumCases(): array
784789
{
785790
$compare = [];

0 commit comments

Comments
 (0)