From 65286ee09a8348f442556fb6e0976b498a6cac29 Mon Sep 17 00:00:00 2001 From: Martin Herndl Date: Wed, 25 May 2022 21:54:18 +0200 Subject: [PATCH] Add `ArrayChunkFunctionReturnTypeExtension` --- conf/config.neon | 5 ++ src/Type/Constant/ConstantArrayType.php | 19 +++++++ .../ArrayChunkFunctionReturnTypeExtension.php | 55 +++++++++++++++++++ .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/array-chunk.php | 42 ++++++++++++++ 5 files changed, 122 insertions(+) create mode 100644 src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/data/array-chunk.php diff --git a/conf/config.neon b/conf/config.neon index c9b573604a..038df2355d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -979,6 +979,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\ArrayChunkFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArrayColumnFunctionReturnTypeExtension tags: diff --git a/src/Type/Constant/ConstantArrayType.php b/src/Type/Constant/ConstantArrayType.php index 0ed0c0ecb5..60558015ea 100644 --- a/src/Type/Constant/ConstantArrayType.php +++ b/src/Type/Constant/ConstantArrayType.php @@ -730,6 +730,25 @@ public function reverse(bool $preserveKeys = false): self return $preserveKeys ? $reversed : $reversed->reindex(); } + /** @param positive-int $length */ + public function chunk(int $length, bool $preserveKeys = false): self + { + $builder = ConstantArrayTypeBuilder::createEmpty(); + + $keyTypesCount = count($this->keyTypes); + for ($i = 0; $i < $keyTypesCount; $i += $length) { + $chunk = $this->slice($i, $length, true); + $builder->setOffsetValueType(null, $preserveKeys ? $chunk : $chunk->getValuesArray()); + } + + $chunks = $builder->getArray(); + if (!$chunks instanceof self) { + throw new ShouldNotHappenException(); + } + + return $chunks; + } + private function reindex(): self { $keyTypes = []; diff --git a/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..28e829357e --- /dev/null +++ b/src/Type/Php/ArrayChunkFunctionReturnTypeExtension.php @@ -0,0 +1,55 @@ +getName() === 'array_chunk'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type + { + if (count($functionCall->getArgs()) < 2) { + return null; + } + + $arrayType = $scope->getType($functionCall->getArgs()[0]->value); + $lengthType = $scope->getType($functionCall->getArgs()[1]->value); + $preserveKeysType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null; + $preserveKeys = $preserveKeysType instanceof ConstantBooleanType ? $preserveKeysType->getValue() : false; + + if (!$arrayType->isIterable()->yes() || !$lengthType instanceof ConstantIntegerType || $lengthType->getValue() < 1) { + return null; + } + + return TypeTraverser::map($arrayType, static function (Type $type, callable $traverse) use ($lengthType, $preserveKeys): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + if ($type instanceof ConstantArrayType) { + return $type->chunk($lengthType->getValue(), $preserveKeys); + } + $chunkType = $preserveKeys ? $type : new ArrayType(new IntegerType(), $type->getIterableValueType()); + return new ArrayType(new IntegerType(), $chunkType); + }); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index c0d0532052..0f581dd8dd 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -650,6 +650,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-chunk.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php'); if (PHP_VERSION_ID >= 80000) { yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php8.php'); diff --git a/tests/PHPStan/Analyser/data/array-chunk.php b/tests/PHPStan/Analyser/data/array-chunk.php new file mode 100644 index 0000000000..68fa0bf3ee --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-chunk.php @@ -0,0 +1,42 @@ +>', array_chunk($arr, 2)); + assertType('array', array_chunk($arr, 2, true)); + + /** @var array $arr */ + assertType('array>', array_chunk($arr, 2)); + assertType('array>', array_chunk($arr, 2, true)); + } + + + public function constantArrays(array $arr): void + { + /** @var array{a: 0, 17: 1, b: 2} $arr */ + assertType('array{array{0, 1}, array{2}}', array_chunk($arr, 2)); + assertType('array{array{a: 0, 17: 1}, array{b: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{0}, array{1}, array{2}}', array_chunk($arr, 1)); + assertType('array{array{a: 0}, array{17: 1}, array{b: 2}}', array_chunk($arr, 1, true)); + } + + public function constantArraysWithOptionalKeys(array $arr): void + { + /** @var array{a: 0, b?: 1, c: 2} $arr */ + assertType('array{array{a: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + assertType('array{array{a: 0, b?: 1, c: 2}}', array_chunk($arr, 3, true)); + assertType('array{array{a: 0}, array{b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 1, true)); + + /** @var array{a?: 0, b?: 1, c?: 2} $arr */ + assertType('array{array{a?: 0, b?: 1, c?: 2}, array{c?: 2}}', array_chunk($arr, 2, true)); + } + +}