Skip to content

Commit e25e29e

Browse files
authored
sort(), rsort() and usort() convert an array to list
1 parent e13a2ac commit e25e29e

File tree

7 files changed

+199
-1
lines changed

7 files changed

+199
-1
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
130130
use PHPStan\ShouldNotHappenException;
131131
use PHPStan\TrinaryLogic;
132+
use PHPStan\Type\Accessory\AccessoryArrayListType;
132133
use PHPStan\Type\Accessory\NonEmptyArrayType;
133134
use PHPStan\Type\ArrayType;
134135
use PHPStan\Type\ClosureType;
@@ -2128,6 +2129,19 @@ static function (): void {
21282129
);
21292130
}
21302131

2132+
if (
2133+
$functionReflection !== null
2134+
&& in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true)
2135+
&& count($expr->getArgs()) >= 1
2136+
) {
2137+
$arrayArg = $expr->getArgs()[0]->value;
2138+
$scope = $scope->assignExpression(
2139+
$arrayArg,
2140+
$this->getArraySortFunctionType($scope->getType($arrayArg)),
2141+
$this->getArraySortFunctionType($scope->getNativeType($arrayArg)),
2142+
);
2143+
}
2144+
21312145
if (
21322146
$functionReflection !== null
21332147
&& $functionReflection->getName() === 'extract'
@@ -3079,6 +3093,24 @@ static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arra
30793093
return $arrayType;
30803094
}
30813095

3096+
private function getArraySortFunctionType(Type $type): Type
3097+
{
3098+
if (!$type->isArray()->yes()) {
3099+
return $type;
3100+
}
3101+
3102+
$isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
3103+
if ($isIterableAtLeastOnce->no()) {
3104+
return $type;
3105+
}
3106+
3107+
$newArrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $type->getIterableValueType()));
3108+
if ($isIterableAtLeastOnce->yes()) {
3109+
$newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
3110+
}
3111+
return $newArrayType;
3112+
}
3113+
30823114
private function getFunctionThrowPoint(
30833115
FunctionReflection $functionReflection,
30843116
?ParametersAcceptor $parametersAcceptor,

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1419,6 +1419,8 @@ public function dataFileAsserts(): iterable
14191419
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7291.php');
14201420
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10264.php');
14211421
yield from $this->gatherAssertTypes(__DIR__ . '/data/conditional-vars.php');
1422+
yield from $this->gatherAssertTypes(__DIR__ . '/data/sort.php');
1423+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3312.php');
14221424
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5961.php');
14231425
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10189.php');
14241426
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10317.php');
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Bug3312;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
function sayHello(): void
8+
{
9+
$arr = ['one' => 'een', 'two' => 'twee', 'three' => 'drie'];
10+
usort($arr, 'strcmp');
11+
assertType("non-empty-list<'drie'|'een'|'twee'>", $arr);
12+
}

tests/PHPStan/Analyser/data/param-out.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ function fooShuffle() {
250250
function fooSort() {
251251
$array = ["foo" => 123, "bar" => 456];
252252
sort($array);
253-
assertType('array{foo: 123, bar: 456}', $array);
253+
assertType('non-empty-list<123|456>', $array);
254254

255255
$emptyArray = [];
256256
sort($emptyArray);
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Sort;
4+
5+
use function PHPStan\Testing\assertNativeType;
6+
use function PHPStan\Testing\assertType;
7+
use function sort;
8+
9+
class Foo
10+
{
11+
public function constantArray(): void
12+
{
13+
$arr = [
14+
4,
15+
'one' => 1,
16+
'five' => 5,
17+
'three' => 3,
18+
];
19+
20+
$arr1 = $arr;
21+
sort($arr1);
22+
assertType('non-empty-list<1|3|4|5>', $arr1);
23+
assertNativeType('non-empty-list<1|3|4|5>', $arr1);
24+
25+
$arr2 = $arr;
26+
rsort($arr2);
27+
assertType('non-empty-list<1|3|4|5>', $arr2);
28+
assertNativeType('non-empty-list<1|3|4|5>', $arr2);
29+
30+
$arr3 = $arr;
31+
usort($arr3, fn(int $a, int $b) => $a <=> $b);
32+
assertType('non-empty-list<1|3|4|5>', $arr3);
33+
assertNativeType('non-empty-list<1|3|4|5>', $arr3);
34+
}
35+
36+
public function constantArrayOptionalKey(): void
37+
{
38+
$arr = [
39+
'one' => 1,
40+
'five' => 5,
41+
];
42+
if (rand(0, 1)) {
43+
$arr['two'] = 2;
44+
}
45+
46+
$arr1 = $arr;
47+
sort($arr1);
48+
assertType('non-empty-list<1|2|5>', $arr1);
49+
assertNativeType('non-empty-list<1|2|5>', $arr1);
50+
51+
$arr2 = $arr;
52+
rsort($arr2);
53+
assertType('non-empty-list<1|2|5>', $arr2);
54+
assertNativeType('non-empty-list<1|2|5>', $arr2);
55+
56+
$arr3 = $arr;
57+
usort($arr3, fn(int $a, int $b) => $a <=> $b);
58+
assertType('non-empty-list<1|2|5>', $arr3);
59+
assertNativeType('non-empty-list<1|2|5>', $arr3);
60+
}
61+
62+
public function constantArrayUnion(): void
63+
{
64+
$arr = rand(0, 1) ? [
65+
'one' => 1,
66+
'five' => 5,
67+
] : [
68+
'two' => 2,
69+
];
70+
71+
$arr1 = $arr;
72+
sort($arr1);
73+
assertType('non-empty-list<1|2|5>', $arr1);
74+
assertNativeType('non-empty-list<1|2|5>', $arr1);
75+
76+
$arr2 = $arr;
77+
rsort($arr2);
78+
assertType('non-empty-list<1|2|5>', $arr2);
79+
assertNativeType('non-empty-list<1|2|5>', $arr2);
80+
81+
$arr3 = $arr;
82+
usort($arr3, fn(int $a, int $b) => $a <=> $b);
83+
assertType('non-empty-list<1|2|5>', $arr3);
84+
assertNativeType('non-empty-list<1|2|5>', $arr3);
85+
}
86+
87+
/** @param array<mixed, string> $arr */
88+
public function normalArray(array $arr): void
89+
{
90+
$arr1 = $arr;
91+
sort($arr1);
92+
assertType('list<string>', $arr1);
93+
assertNativeType('list<mixed>', $arr1);
94+
95+
$arr2 = $arr;
96+
rsort($arr2);
97+
assertType('list<string>', $arr2);
98+
assertNativeType('list<mixed>', $arr2);
99+
100+
$arr3 = $arr;
101+
usort($arr3, fn(int $a, int $b) => $a <=> $b);
102+
assertType('list<string>', $arr3);
103+
assertNativeType('list<mixed>', $arr3);
104+
}
105+
106+
public function mixed($arr): void
107+
{
108+
$arr1 = $arr;
109+
sort($arr1);
110+
assertType('mixed', $arr1);
111+
assertNativeType('mixed', $arr1);
112+
113+
$arr2 = $arr;
114+
rsort($arr2);
115+
assertType('mixed', $arr2);
116+
assertNativeType('mixed', $arr2);
117+
118+
$arr3 = $arr;
119+
usort($arr3, fn(int $a, int $b) => $a <=> $b);
120+
assertType('mixed', $arr3);
121+
assertNativeType('mixed', $arr3);
122+
}
123+
124+
public function notArray(): void
125+
{
126+
$arr = 'foo';
127+
sort($arr);
128+
assertType("'foo'", $arr);
129+
}
130+
}

tests/PHPStan/Rules/Comparison/NumberComparisonOperatorsConstantConditionRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,10 @@ public function testBug5005(): void
225225
$this->analyse([__DIR__ . '/data/bug-5005.php'], []);
226226
}
227227

228+
public function testBug6467(): void
229+
{
230+
$this->treatPhpDocTypesAsCertain = true;
231+
$this->analyse([__DIR__ . '/data/bug-6467.php'], []);
232+
}
233+
228234
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug6467;
4+
5+
class Comparison
6+
{
7+
public function compare(): void
8+
{
9+
$values = $sortedValues = [0.5, 1, 2, 0.8, 0.4];
10+
sort($sortedValues);
11+
$expected = $sortedValues[2] + 0.05;
12+
foreach (array_fill(0, 5, null) as $index => $null) {
13+
$success = $values[$index] < $expected;
14+
}
15+
}
16+
}

0 commit comments

Comments
 (0)