Skip to content

Commit c107652

Browse files
authored
Add Type::chunkArray()
1 parent 3e5195b commit c107652

19 files changed

+147
-68
lines changed

phpstan-baseline.neon

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,11 +1283,6 @@ parameters:
12831283
count: 4
12841284
path: src/Type/ObjectWithoutClassType.php
12851285

1286-
-
1287-
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantBooleanType is error\\-prone and deprecated\\. Use Type\\:\\:isTrue\\(\\) or Type\\:\\:isFalse\\(\\) instead\\.$#"
1288-
count: 1
1289-
path: src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php
1290-
12911286
-
12921287
message: "#^Doing instanceof PHPStan\\\\Type\\\\Constant\\\\ConstantArrayType is error\\-prone and deprecated\\. Use Type\\:\\:getConstantArrays\\(\\) instead\\.$#"
12931288
count: 2

src/Type/Accessory/AccessoryArrayListType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ public function getValuesArray(): Type
194194
return $this;
195195
}
196196

197+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
198+
{
199+
return $this;
200+
}
201+
197202
public function fillKeysArray(Type $valueType): Type
198203
{
199204
return new MixedType();

src/Type/Accessory/HasOffsetType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ public function unsetOffset(Type $offsetType): Type
175175
return $this;
176176
}
177177

178+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
179+
{
180+
return new NonEmptyArrayType();
181+
}
182+
178183
public function fillKeysArray(Type $valueType): Type
179184
{
180185
return new NonEmptyArrayType();

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ public function getValuesArray(): Type
209209
return new NonEmptyArrayType();
210210
}
211211

212+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
213+
{
214+
return new NonEmptyArrayType();
215+
}
216+
212217
public function fillKeysArray(Type $valueType): Type
213218
{
214219
return new NonEmptyArrayType();

src/Type/Accessory/NonEmptyArrayType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ public function getValuesArray(): Type
179179
return $this;
180180
}
181181

182+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
183+
{
184+
return $this;
185+
}
186+
182187
public function fillKeysArray(Type $valueType): Type
183188
{
184189
return $this;

src/Type/Accessory/OversizedArrayType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ public function getValuesArray(): Type
175175
return $this;
176176
}
177177

178+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
179+
{
180+
return $this;
181+
}
182+
178183
public function fillKeysArray(Type $valueType): Type
179184
{
180185
return $this;

src/Type/ArrayType.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,20 @@ public function unsetOffset(Type $offsetType): Type
513513
return $this;
514514
}
515515

516+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
517+
{
518+
$chunkType = $preserveKeys->yes()
519+
? $this
520+
: AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $this->getIterableValueType()));
521+
$chunkType = TypeCombinator::intersect($chunkType, new NonEmptyArrayType());
522+
523+
$arrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $chunkType));
524+
525+
return $this->isIterableAtLeastOnce()->yes()
526+
? TypeCombinator::intersect($arrayType, new NonEmptyArrayType())
527+
: $arrayType;
528+
}
529+
516530
public function fillKeysArray(Type $valueType): Type
517531
{
518532
$itemType = $this->getItemType();

src/Type/Constant/ConstantArrayType.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ class ConstantArrayType extends ArrayType implements ConstantType
7373
{
7474

7575
private const DESCRIBE_LIMIT = 8;
76+
private const CHUNK_FINITE_TYPES_LIMIT = 5;
7677

7778
private TrinaryLogic $isList;
7879

@@ -780,6 +781,36 @@ public function unsetOffset(Type $offsetType): Type
780781
return new self($this->keyTypes, $this->valueTypes, $this->nextAutoIndexes, $optionalKeys, $isList);
781782
}
782783

784+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
785+
{
786+
$biggerOne = IntegerRangeType::fromInterval(1, null);
787+
$finiteTypes = $lengthType->getFiniteTypes();
788+
if ($biggerOne->isSuperTypeOf($lengthType)->yes() && count($finiteTypes) < self::CHUNK_FINITE_TYPES_LIMIT) {
789+
$results = [];
790+
foreach ($finiteTypes as $finiteType) {
791+
if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) {
792+
return parent::chunkArray($lengthType, $preserveKeys);
793+
}
794+
795+
$length = $finiteType->getValue();
796+
797+
$builder = ConstantArrayTypeBuilder::createEmpty();
798+
799+
$keyTypesCount = count($this->keyTypes);
800+
for ($i = 0; $i < $keyTypesCount; $i += $length) {
801+
$chunk = $this->slice($i, $length, true);
802+
$builder->setOffsetValueType(null, $preserveKeys->yes() ? $chunk : $chunk->getValuesArray());
803+
}
804+
805+
$results[] = $builder->getArray();
806+
}
807+
808+
return TypeCombinator::union(...$results);
809+
}
810+
811+
return parent::chunkArray($lengthType, $preserveKeys);
812+
}
813+
783814
public function fillKeysArray(Type $valueType): Type
784815
{
785816
$builder = ConstantArrayTypeBuilder::createEmpty();
@@ -1185,7 +1216,10 @@ public function reverse(bool $preserveKeys = false): self
11851216
return $this->reverseConstantArray(TrinaryLogic::createFromBoolean($preserveKeys));
11861217
}
11871218

1188-
/** @param positive-int $length */
1219+
/**
1220+
* @deprecated Use chunkArray() instead
1221+
* @param positive-int $length
1222+
*/
11891223
public function chunk(int $length, bool $preserveKeys = false): self
11901224
{
11911225
$builder = ConstantArrayTypeBuilder::createEmpty();

src/Type/IntersectionType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,11 @@ public function getValuesArray(): Type
724724
return $this->intersectTypes(static fn (Type $type): Type => $type->getValuesArray());
725725
}
726726

727+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
728+
{
729+
return $this->intersectTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
730+
}
731+
727732
public function fillKeysArray(Type $valueType): Type
728733
{
729734
return $this->intersectTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));

src/Type/MixedType.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,15 @@ public function getValuesArray(): Type
189189
return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)));
190190
}
191191

192+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
193+
{
194+
if ($this->isArray()->no()) {
195+
return new ErrorType();
196+
}
197+
198+
return AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new MixedType($this->isExplicitMixed)));
199+
}
200+
192201
public function fillKeysArray(Type $valueType): Type
193202
{
194203
if ($this->isArray()->no()) {

src/Type/NeverType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ public function getValuesArray(): Type
296296
return new NeverType();
297297
}
298298

299+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
300+
{
301+
return new NeverType();
302+
}
303+
299304
public function fillKeysArray(Type $valueType): Type
300305
{
301306
return new NeverType();

src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php

Lines changed: 6 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,17 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Php\PhpVersion;
88
use PHPStan\Reflection\FunctionReflection;
9-
use PHPStan\Type\Accessory\AccessoryArrayListType;
10-
use PHPStan\Type\Accessory\NonEmptyArrayType;
11-
use PHPStan\Type\ArrayType;
129
use PHPStan\Type\Constant\ConstantBooleanType;
13-
use PHPStan\Type\Constant\ConstantIntegerType;
1410
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
1511
use PHPStan\Type\IntegerRangeType;
16-
use PHPStan\Type\IntegerType;
1712
use PHPStan\Type\NeverType;
1813
use PHPStan\Type\NullType;
1914
use PHPStan\Type\Type;
20-
use PHPStan\Type\TypeCombinator;
2115
use function count;
2216

2317
final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
2418
{
2519

26-
private const FINITE_TYPES_LIMIT = 5;
27-
2820
public function __construct(private PhpVersion $phpVersion)
2921
{
3022
}
@@ -41,68 +33,20 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
4133
}
4234

4335
$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
44-
$lengthType = $scope->getType($functionCall->getArgs()[1]->value);
45-
if (isset($functionCall->getArgs()[2])) {
46-
$preserveKeysType = $scope->getType($functionCall->getArgs()[2]->value);
47-
$preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : null;
48-
} else {
49-
$preserveKeys = false;
36+
if ($arrayType->isArray()->no()) {
37+
return $this->phpVersion->arrayFunctionsReturnNullWithNonArray() ? new NullType() : new NeverType();
5038
}
5139

40+
$lengthType = $scope->getType($functionCall->getArgs()[1]->value);
5241
$negativeOrZero = IntegerRangeType::fromInterval(null, 0);
5342
if ($negativeOrZero->isSuperTypeOf($lengthType)->yes()) {
5443
return $this->phpVersion->throwsValueErrorForInternalFunctions() ? new NeverType() : new NullType();
5544
}
5645

57-
if (!$arrayType->isArray()->yes()) {
58-
return null;
59-
}
60-
61-
if ($preserveKeys !== null) {
62-
$constantArrays = $arrayType->getConstantArrays();
63-
$biggerOne = IntegerRangeType::fromInterval(1, null);
64-
$finiteTypes = $lengthType->getFiniteTypes();
65-
if (count($constantArrays) > 0
66-
&& $biggerOne->isSuperTypeOf($lengthType)->yes()
67-
&& count($finiteTypes) < self::FINITE_TYPES_LIMIT
68-
) {
69-
$results = [];
70-
foreach ($constantArrays as $constantArray) {
71-
foreach ($finiteTypes as $finiteType) {
72-
if (!$finiteType instanceof ConstantIntegerType || $finiteType->getValue() < 1) {
73-
return null;
74-
}
75-
76-
$results[] = $constantArray->chunk($finiteType->getValue(), $preserveKeys);
77-
}
78-
}
79-
80-
return TypeCombinator::union(...$results);
81-
}
82-
}
83-
84-
$chunkType = self::getChunkType($arrayType, $preserveKeys);
85-
86-
$resultType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $chunkType));
87-
if ($arrayType->isIterableAtLeastOnce()->yes()) {
88-
$resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType());
89-
}
90-
91-
return $resultType;
92-
}
93-
94-
private static function getChunkType(Type $type, ?bool $preserveKeys): Type
95-
{
96-
if ($preserveKeys === null) {
97-
$chunkType = new ArrayType(TypeCombinator::union($type->getIterableKeyType(), new IntegerType()), $type->getIterableValueType());
98-
} elseif ($preserveKeys) {
99-
$chunkType = $type;
100-
} else {
101-
$chunkType = new ArrayType(new IntegerType(), $type->getIterableValueType());
102-
$chunkType = AccessoryArrayListType::intersectWith($chunkType);
103-
}
46+
$preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : new ConstantBooleanType(false);
47+
$preserveKeys = (new ConstantBooleanType(true))->isSuperTypeOf($preserveKeysType);
10448

105-
return TypeCombinator::intersect($chunkType, new NonEmptyArrayType());
49+
return $arrayType->chunkArray($lengthType, $preserveKeys);
10650
}
10751

10852
}

src/Type/StaticType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,11 @@ public function getValuesArray(): Type
415415
return $this->getStaticObjectType()->getValuesArray();
416416
}
417417

418+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
419+
{
420+
return $this->getStaticObjectType()->chunkArray($lengthType, $preserveKeys);
421+
}
422+
418423
public function fillKeysArray(Type $valueType): Type
419424
{
420425
return $this->getStaticObjectType()->fillKeysArray($valueType);

src/Type/Traits/LateResolvableTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,11 @@ public function getValuesArray(): Type
262262
return $this->resolve()->getValuesArray();
263263
}
264264

265+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
266+
{
267+
return $this->resolve()->chunkArray($lengthType, $preserveKeys);
268+
}
269+
265270
public function fillKeysArray(Type $valueType): Type
266271
{
267272
return $this->resolve()->fillKeysArray($valueType);

src/Type/Traits/MaybeArrayTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function getValuesArray(): Type
4949
return new ErrorType();
5050
}
5151

52+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
53+
{
54+
return new ErrorType();
55+
}
56+
5257
public function fillKeysArray(Type $valueType): Type
5358
{
5459
return new ErrorType();

src/Type/Traits/NonArrayTypeTrait.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function getValuesArray(): Type
4949
return new ErrorType();
5050
}
5151

52+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
53+
{
54+
return new ErrorType();
55+
}
56+
5257
public function fillKeysArray(Type $valueType): Type
5358
{
5459
return new ErrorType();

src/Type/Type.php

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

152152
public function getValuesArray(): Type;
153153

154+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type;
155+
154156
public function fillKeysArray(Type $valueType): Type;
155157

156158
public function flipArray(): Type;

src/Type/UnionType.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,11 @@ public function getValuesArray(): Type
701701
return $this->unionTypes(static fn (Type $type): Type => $type->getValuesArray());
702702
}
703703

704+
public function chunkArray(Type $lengthType, TrinaryLogic $preserveKeys): Type
705+
{
706+
return $this->unionTypes(static fn (Type $type): Type => $type->chunkArray($lengthType, $preserveKeys));
707+
}
708+
704709
public function fillKeysArray(Type $valueType): Type
705710
{
706711
return $this->unionTypes(static fn (Type $type): Type => $type->fillKeysArray($valueType));

tests/PHPStan/Analyser/nsrt/array-chunk.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,26 @@ function testLimits(array $arr, int $oneToFour, int $tooBig) {
7474
assertType('non-empty-list<non-empty-list<0|1|2|3>>', array_chunk($arr, $tooBig));
7575
}
7676

77+
/** @param array<string, string> $map */
78+
public function offsets(array $arr, array $map): void
79+
{
80+
if (array_key_exists('foo', $arr)) {
81+
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
82+
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
83+
}
84+
if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') {
85+
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
86+
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
87+
}
88+
89+
if (array_key_exists('foo', $map)) {
90+
assertType('non-empty-list<non-empty-list<string>>', array_chunk($map, 2));
91+
assertType('non-empty-list<non-empty-array<string, string>>', array_chunk($map, 2, true));
92+
}
93+
if (array_key_exists('foo', $map) && $map['foo'] === 'bar') {
94+
assertType('non-empty-list<non-empty-list<string>>', array_chunk($map, 2));
95+
assertType('non-empty-list<non-empty-array<string, string>>', array_chunk($map, 2, true));
96+
}
97+
}
7798

7899
}

0 commit comments

Comments
 (0)