Skip to content

Commit 2aed653

Browse files
github-actions[bot]phpstan-bot
authored andcommitted
Handle ksort/krsort key reordering for constant arrays
- ksort/krsort now reorder ConstantArrayType entries by key value - After ksort, array_values() returns values in the correct sorted order - Correctly computes isList for sorted arrays (ksort on a list stays a list) - Updated existing bug-10627 test expectations to match correct behavior - New regression test in tests/PHPStan/Analyser/nsrt/bug-11569.php Closes phpstan/phpstan#11569
1 parent ba4fe14 commit 2aed653

File tree

3 files changed

+173
-5
lines changed

3 files changed

+173
-5
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
use UnhandledMatchError;
209209
use function array_fill_keys;
210210
use function array_filter;
211+
use function array_flip;
211212
use function array_key_exists;
212213
use function array_key_last;
213214
use function array_keys;
@@ -3049,7 +3050,25 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto
30493050

30503051
if (
30513052
$functionReflection !== null
3052-
&& in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true)
3053+
&& in_array($functionReflection->getName(), ['ksort', 'krsort'], true)
3054+
&& count($normalizedExpr->getArgs()) >= 1
3055+
) {
3056+
$arrayArg = $normalizedExpr->getArgs()[0]->value;
3057+
$reverse = $functionReflection->getName() === 'krsort';
3058+
3059+
$scope = $this->processVirtualAssign(
3060+
$scope,
3061+
$storage,
3062+
$stmt,
3063+
$arrayArg,
3064+
new NativeTypeExpr($this->getArrayKsortFunctionType($scope->getType($arrayArg), $reverse), $this->getArrayKsortFunctionType($scope->getNativeType($arrayArg), $reverse)),
3065+
$nodeCallback,
3066+
)->getScope();
3067+
}
3068+
3069+
if (
3070+
$functionReflection !== null
3071+
&& in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'uasort', 'uksort'], true)
30533072
&& count($normalizedExpr->getArgs()) >= 1
30543073
) {
30553074
$arrayArg = $normalizedExpr->getArgs()[0]->value;
@@ -4734,6 +4753,81 @@ private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
47344753
});
47354754
}
47364755

4756+
private function getArrayKsortFunctionType(Type $type, bool $reverse): Type
4757+
{
4758+
$isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
4759+
if ($isIterableAtLeastOnce->no()) {
4760+
return $type;
4761+
}
4762+
4763+
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce, $reverse): Type {
4764+
if ($type instanceof UnionType) {
4765+
return $traverse($type);
4766+
}
4767+
4768+
$constantArrays = $type->getConstantArrays();
4769+
if (count($constantArrays) > 0) {
4770+
$types = [];
4771+
foreach ($constantArrays as $constantArray) {
4772+
$keyTypes = $constantArray->getKeyTypes();
4773+
$valueTypes = $constantArray->getValueTypes();
4774+
$optionalKeys = $constantArray->getOptionalKeys();
4775+
4776+
$indices = array_keys($keyTypes);
4777+
usort($indices, static function (int $a, int $b) use ($keyTypes, $reverse): int {
4778+
$keyA = $keyTypes[$a]->getValue();
4779+
$keyB = $keyTypes[$b]->getValue();
4780+
$result = $keyA <=> $keyB;
4781+
return $reverse ? -$result : $result;
4782+
});
4783+
4784+
$sortedKeyTypes = [];
4785+
$sortedValueTypes = [];
4786+
$newOptionalKeys = [];
4787+
$optionalKeysSet = array_flip($optionalKeys);
4788+
foreach ($indices as $newIndex => $oldIndex) {
4789+
$sortedKeyTypes[] = $keyTypes[$oldIndex];
4790+
$sortedValueTypes[] = $valueTypes[$oldIndex];
4791+
if (!isset($optionalKeysSet[$oldIndex])) {
4792+
continue;
4793+
}
4794+
4795+
$newOptionalKeys[] = $newIndex;
4796+
}
4797+
4798+
$isList = TrinaryLogic::createNo();
4799+
$isSequential = true;
4800+
foreach ($sortedKeyTypes as $i => $keyType) {
4801+
if (!$keyType instanceof ConstantIntegerType || $keyType->getValue() !== $i) {
4802+
$isSequential = false;
4803+
break;
4804+
}
4805+
}
4806+
if ($isSequential) {
4807+
$isList = $constantArray->isList();
4808+
}
4809+
4810+
$types[] = new ConstantArrayType(
4811+
$sortedKeyTypes,
4812+
$sortedValueTypes,
4813+
$constantArray->getNextAutoIndexes(),
4814+
$newOptionalKeys,
4815+
$isList,
4816+
);
4817+
}
4818+
4819+
return TypeCombinator::union(...$types);
4820+
}
4821+
4822+
$newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
4823+
if ($isIterableAtLeastOnce->yes()) {
4824+
$newArrayType = new IntersectionType([$newArrayType, new NonEmptyArrayType()]);
4825+
}
4826+
4827+
return $newArrayType;
4828+
});
4829+
}
4830+
47374831
private function getFunctionThrowPoint(
47384832
FunctionReflection $functionReflection,
47394833
?ParametersAcceptor $parametersAcceptor,

tests/PHPStan/Analyser/nsrt/bug-10627.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public function sayHello5(): void
4444
$list = ['A', 'C', 'B'];
4545
ksort($list);
4646
assertType("array{'A', 'C', 'B'}", $list);
47-
assertType('bool', array_is_list($list));
47+
assertType('true', array_is_list($list));
4848
}
4949

5050
public function sayHello6(): void
@@ -71,8 +71,8 @@ public function sayHello8(): void
7171
{
7272
$list = ['A', 'C', 'B'];
7373
krsort($list);
74-
assertType("array{'A', 'C', 'B'}", $list);
75-
assertType('bool', array_is_list($list));
74+
assertType("array{2: 'B', 1: 'C', 0: 'A'}", $list);
75+
assertType('false', array_is_list($list));
7676
}
7777

7878
/**
@@ -89,7 +89,7 @@ public function sayHello10(): void
8989
{
9090
$list = ['a' => 'A', 'c' => 'C', 'b' => 'B'];
9191
krsort($list);
92-
assertType("array{a: 'A', c: 'C', b: 'B'}", $list);
92+
assertType("array{c: 'C', b: 'B', a: 'A'}", $list);
9393
assertType('false', array_is_list($list));
9494
}
9595
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug11569;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/** @return array{name: string, age: int} */
10+
public function getFoo(): array
11+
{
12+
return ['name' => 'John Doe', 'age' => 30];
13+
}
14+
15+
public function testKsort(): void
16+
{
17+
$value = ['name' => 'John Doe', 'age' => 30];
18+
ksort($value);
19+
assertType("array{age: 30, name: 'John Doe'}", $value);
20+
}
21+
22+
public function testKrsort(): void
23+
{
24+
$value = ['name' => 'John Doe', 'age' => 30];
25+
krsort($value);
26+
assertType("array{name: 'John Doe', age: 30}", $value);
27+
}
28+
29+
public function testKsortWithArrayValues(): void
30+
{
31+
$data = $this->getFoo();
32+
ksort($data);
33+
assertType('array{age: int, name: string}', $data);
34+
$values = array_values($data);
35+
assertType('array{int, string}', $values);
36+
}
37+
38+
public function testKsortWithArrayCombine(): void
39+
{
40+
$data = $this->getFoo();
41+
ksort($data);
42+
$values = array_values($data);
43+
$result = array_combine(['age', 'name'], $values);
44+
assertType('array{age: int, name: string}', $result);
45+
}
46+
47+
public function testKsortIntegerKeys(): void
48+
{
49+
$list = ['A', 'C', 'B'];
50+
ksort($list);
51+
assertType("array{'A', 'C', 'B'}", $list);
52+
}
53+
54+
public function testKrsortIntegerKeys(): void
55+
{
56+
$list = ['A', 'C', 'B'];
57+
krsort($list);
58+
assertType("array{2: 'B', 1: 'C', 0: 'A'}", $list);
59+
}
60+
61+
public function testKsortStringKeys(): void
62+
{
63+
$value = ['c' => 3, 'a' => 1, 'b' => 2];
64+
ksort($value);
65+
assertType('array{a: 1, b: 2, c: 3}', $value);
66+
}
67+
68+
public function testKrsortStringKeys(): void
69+
{
70+
$value = ['c' => 3, 'a' => 1, 'b' => 2];
71+
krsort($value);
72+
assertType('array{c: 3, b: 2, a: 1}', $value);
73+
}
74+
}

0 commit comments

Comments
 (0)