Skip to content

Commit eac5b1c

Browse files
authored
Add ArrayChunkFunctionReturnTypeExtension
1 parent 735c822 commit eac5b1c

File tree

5 files changed

+136
-0
lines changed

5 files changed

+136
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,11 @@ services:
979979
tags:
980980
- phpstan.broker.dynamicFunctionReturnTypeExtension
981981

982+
-
983+
class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension
984+
tags:
985+
- phpstan.broker.dynamicFunctionReturnTypeExtension
986+
982987
-
983988
class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension
984989
tags:

src/Type/Constant/ConstantArrayType.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,25 @@ public function reverse(bool $preserveKeys = false): self
730730
return $preserveKeys ? $reversed : $reversed->reindex();
731731
}
732732

733+
/** @param positive-int $length */
734+
public function chunk(int $length, bool $preserveKeys = false): self
735+
{
736+
$builder = ConstantArrayTypeBuilder::createEmpty();
737+
738+
$keyTypesCount = count($this->keyTypes);
739+
for ($i = 0; $i < $keyTypesCount; $i += $length) {
740+
$chunk = $this->slice($i, $length, true);
741+
$builder->setOffsetValueType(null, $preserveKeys ? $chunk : $chunk->getValuesArray());
742+
}
743+
744+
$chunks = $builder->getArray();
745+
if (!$chunks instanceof self) {
746+
throw new ShouldNotHappenException();
747+
}
748+
749+
return $chunks;
750+
}
751+
733752
private function reindex(): self
734753
{
735754
$keyTypes = [];
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\Accessory\NonEmptyArrayType;
9+
use PHPStan\Type\ArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayType;
11+
use PHPStan\Type\Constant\ConstantBooleanType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
13+
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\IntersectionType;
16+
use PHPStan\Type\Type;
17+
use PHPStan\Type\TypeCombinator;
18+
use PHPStan\Type\TypeTraverser;
19+
use PHPStan\Type\UnionType;
20+
use function count;
21+
22+
final class ArrayChunkFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
23+
{
24+
25+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
26+
{
27+
return $functionReflection->getName() === 'array_chunk';
28+
}
29+
30+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
31+
{
32+
if (count($functionCall->getArgs()) < 2) {
33+
return null;
34+
}
35+
36+
$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
37+
$lengthType = $scope->getType($functionCall->getArgs()[1]->value);
38+
$preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
39+
$preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : false;
40+
41+
if (!$arrayType->isArray()->yes() || !$lengthType instanceof ConstantIntegerType || $lengthType->getValue() < 1) {
42+
return null;
43+
}
44+
45+
return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($lengthType, $preserveKeys): Type {
46+
if ($type instanceof UnionType || $type instanceof IntersectionType) {
47+
return $traverse($type);
48+
}
49+
50+
if ($type instanceof ConstantArrayType) {
51+
return $type->chunk($lengthType->getValue(), $preserveKeys);
52+
}
53+
54+
$chunkType = $preserveKeys ? $type : new ArrayType(new IntegerType(), $type->getIterableValueType());
55+
$chunkType = TypeCombinator::intersect($chunkType, new NonEmptyArrayType());
56+
57+
$resultType = new ArrayType(new IntegerType(), $chunkType);
58+
if ($type->isIterableAtLeastOnce()->yes()) {
59+
$resultType = TypeCombinator::intersect($type, new NonEmptyArrayType());
60+
}
61+
return $resultType;
62+
});
63+
}
64+
65+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ public function dataFileAsserts(): iterable
650650
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php');
651651
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php');
652652

653+
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php');
653654
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php');
654655
if (PHP_VERSION_ID >= 80000) {
655656
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php8.php');
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ArrayChunk;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class Foo
8+
{
9+
10+
public function generalArrays(array $arr): void
11+
{
12+
/** @var mixed[] $arr */
13+
assertType('array<int, non-empty-array<int, mixed>>', array_chunk($arr, 2));
14+
assertType('array<int, non-empty-array>', array_chunk($arr, 2, true));
15+
16+
/** @var array<string, int> $arr */
17+
assertType('array<int, non-empty-array<int, int>>', array_chunk($arr, 2));
18+
assertType('array<int, non-empty-array<string, int>>', array_chunk($arr, 2, true));
19+
20+
/** @var non-empty-array<int|string, bool> $arr */
21+
assertType('non-empty-array<int, non-empty-array<int, bool>>', array_chunk($arr, 1));
22+
assertType('non-empty-array<int, non-empty-array<int|string, bool>>', array_chunk($arr, 1, true));
23+
}
24+
25+
26+
public function constantArrays(array $arr): void
27+
{
28+
/** @var array{a: 0, 17: 1, b: 2} $arr */
29+
assertType('array{array{0, 1}, array{2}}', array_chunk($arr, 2));
30+
assertType('array{array{a: 0, 17: 1}, array{b: 2}}', array_chunk($arr, 2, true));
31+
assertType('array{array{0}, array{1}, array{2}}', array_chunk($arr, 1));
32+
assertType('array{array{a: 0}, array{17: 1}, array{b: 2}}', array_chunk($arr, 1, true));
33+
}
34+
35+
public function constantArraysWithOptionalKeys(array $arr): void
36+
{
37+
/** @var array{a: 0, b?: 1, c: 2} $arr */
38+
assertType('array{array{a: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true));
39+
assertType('array{array{a: 0, b?: 1, c: 2}}', array_chunk($arr, 3, true));
40+
assertType('array{array{a: 0}, array{b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 1, true));
41+
42+
/** @var array{a?: 0, b?: 1, c?: 2} $arr */
43+
assertType('array{array{a?: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true));
44+
}
45+
46+
}

0 commit comments

Comments
 (0)