From 034f7313064b207883a13f1e25a76975560215b9 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 23 Feb 2024 14:43:10 +0100 Subject: [PATCH] Some sort functions do not preserve a list --- src/Analyser/NodeScopeResolver.php | 40 +++++++++- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/bug-10627.php | 78 +++++++++++++++++++ tests/PHPStan/Analyser/data/param-out.php | 4 +- 4 files changed, 118 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-10627.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 92109be978..1e3baf610b 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2192,8 +2192,21 @@ static function (): void { $arrayArg = $expr->getArgs()[0]->value; $scope = $scope->assignExpression( $arrayArg, - $this->getArraySortFunctionType($scope->getType($arrayArg)), - $this->getArraySortFunctionType($scope->getNativeType($arrayArg)), + $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)), + ); + } + + if ( + $functionReflection !== null + && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true) + && count($expr->getArgs()) >= 1 + ) { + $arrayArg = $expr->getArgs()[0]->value; + $scope = $scope->assignExpression( + $arrayArg, + $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), + $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)), ); } @@ -3157,7 +3170,7 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra return $arrayType; } - private function getArraySortFunctionType(Type $type): Type + private function getArraySortPreserveListFunctionType(Type $type): Type { $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); if ($isIterableAtLeastOnce->no()) { @@ -3182,6 +3195,27 @@ private function getArraySortFunctionType(Type $type): Type }); } + private function getArraySortDoNotPreserveListFunctionType(Type $type): Type + { + $isIterableAtLeastOnce = $type->isIterableAtLeastOnce(); + if ($isIterableAtLeastOnce->no()) { + return $type; + } + + return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type { + if ($type instanceof UnionType) { + return $traverse($type); + } + + $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType()); + if ($isIterableAtLeastOnce->yes()) { + $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType()); + } + + return $newArrayType; + }); + } + private function getFunctionThrowPoint( FunctionReflection $functionReflection, ?ParametersAcceptor $parametersAcceptor, diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index b767cc5b37..4cc7acf4d2 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -654,6 +654,7 @@ public function dataFileAsserts(): iterable } yield from $this->gatherAssertTypes(__DIR__ . '/data/never.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10627.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/native-intersection.php'); diff --git a/tests/PHPStan/Analyser/data/bug-10627.php b/tests/PHPStan/Analyser/data/bug-10627.php new file mode 100644 index 0000000000..f5c9f59cab --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-10627.php @@ -0,0 +1,78 @@ +", $list); + } + + public function sayHello2(): void + { + $list = ['A', 'C', 'B']; + natsort($list); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello3(): void + { + $list = ['A', 'C', 'B']; + arsort($list); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello4(): void + { + $list = ['A', 'C', 'B']; + asort($list); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello5(): void + { + $list = ['A', 'C', 'B']; + ksort($list); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello6(): void + { + $list = ['A', 'C', 'B']; + uasort($list, function () { + + }); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello7(): void + { + $list = ['A', 'C', 'B']; + uksort($list, function () { + + }); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + public function sayHello8(): void + { + $list = ['A', 'C', 'B']; + krsort($list); + assertType("non-empty-array<0|1|2, 'A'|'B'|'C'>", $list); + } + + /** + * @param list $list + * @return void + */ + public function sayHello9(array $list): void + { + krsort($list); + assertType("array, string>", $list); + } +} diff --git a/tests/PHPStan/Analyser/data/param-out.php b/tests/PHPStan/Analyser/data/param-out.php index 4d59b01db5..4723f1d85f 100644 --- a/tests/PHPStan/Analyser/data/param-out.php +++ b/tests/PHPStan/Analyser/data/param-out.php @@ -221,7 +221,7 @@ function foo15() { $manifest, "fooCompare" ); - assertType('array{1, 2, 3}', $manifest); + assertType('non-empty-array<0|1|2, 1|2|3>', $manifest); } function fooSpaceship (string $a, string $b): int { @@ -234,7 +234,7 @@ function foo16() { $array, "fooSpaceship" ); - assertType('array{1, 2}', $array); + assertType('non-empty-array<0|1, 1|2>', $array); } function fooShuffle() {