Skip to content

Commit 4b77c01

Browse files
staabmondrejmirtes
authored andcommitted
array_slice() returns non-empty array for existing offsets and positive limit
1 parent 0072db2 commit 4b77c01

File tree

4 files changed

+127
-8
lines changed

4 files changed

+127
-8
lines changed

src/Type/Php/ArraySliceFunctionReturnTypeExtension.php

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
use PhpParser\Node\Expr\FuncCall;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\Accessory\NonEmptyArrayType;
89
use PHPStan\Type\Constant\ConstantArrayType;
910
use PHPStan\Type\Constant\ConstantIntegerType;
1011
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\IntegerRangeType;
1113
use PHPStan\Type\Type;
1214
use PHPStan\Type\TypeCombinator;
1315
use function count;
@@ -22,24 +24,25 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo
2224

2325
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): ?Type
2426
{
25-
if (count($functionCall->getArgs()) < 1) {
27+
$args = $functionCall->getArgs();
28+
if (count($args) < 1) {
2629
return null;
2730
}
2831

29-
$valueType = $scope->getType($functionCall->getArgs()[0]->value);
32+
$valueType = $scope->getType($args[0]->value);
3033
if (!$valueType->isArray()->yes()) {
3134
return null;
3235
}
3336

34-
$offsetType = isset($functionCall->getArgs()[1]) ? $scope->getType($functionCall->getArgs()[1]->value) : null;
35-
$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0;
37+
$offsetType = isset($args[1]) ? $scope->getType($args[1]->value) : null;
38+
$limitType = isset($args[2]) ? $scope->getType($args[2]->value) : null;
3639

3740
$constantArrays = $valueType->getConstantArrays();
3841
if (count($constantArrays) > 0) {
39-
$limitType = isset($functionCall->getArgs()[2]) ? $scope->getType($functionCall->getArgs()[2]->value) : null;
40-
$limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null;
42+
$preserveKeysType = isset($args[3]) ? $scope->getType($args[3]->value) : null;
4143

42-
$preserveKeysType = isset($functionCall->getArgs()[3]) ? $scope->getType($functionCall->getArgs()[3]->value) : null;
44+
$offset = $offsetType instanceof ConstantIntegerType ? $offsetType->getValue() : 0;
45+
$limit = $limitType instanceof ConstantIntegerType ? $limitType->getValue() : null;
4346
$preserveKeys = $preserveKeysType !== null && $preserveKeysType->isTrue()->yes();
4447

4548
$results = [];
@@ -51,7 +54,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
5154
}
5255

5356
if ($valueType->isIterableAtLeastOnce()->yes()) {
54-
return TypeCombinator::union($valueType, new ConstantArrayType([], []));
57+
$optionalOffsetsType = TypeCombinator::union($valueType, new ConstantArrayType([], []));
58+
59+
$zero = new ConstantIntegerType(0);
60+
if (
61+
($offsetType === null || $zero->isSuperTypeOf($offsetType)->yes())
62+
&& ($limitType === null || IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($limitType)->yes())
63+
) {
64+
return TypeCombinator::intersect($optionalOffsetsType, new NonEmptyArrayType());
65+
}
66+
67+
return $optionalOffsetsType;
5568
}
5669

5770
return $valueType;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,7 @@ public function dataFileAsserts(): iterable
14981498
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10187.php');
14991499
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5309.php');
15001500
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10834.php');
1501+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10721.php');
15011502
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-11035.php');
15021503
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10952.php');
15031504
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10952b.php');
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug10721;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
final class HandpickedWordlistProvider
8+
{
9+
/**
10+
* @return non-empty-list<non-empty-string>
11+
*/
12+
public function retrieve(?int $limit = 20): array
13+
{
14+
$list = [
15+
'zib',
16+
'zib 2',
17+
'zeit im bild',
18+
'soko',
19+
'landkrimi',
20+
'tatort',
21+
];
22+
23+
assertType("array{'zib', 'zib 2', 'zeit im bild', 'soko', 'landkrimi', 'tatort'}", $list);
24+
shuffle($list);
25+
assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", $list);
26+
27+
assertType("non-empty-array<0|1|2|3|4|5, 'landkrimi'|'soko'|'tatort'|'zeit im bild'|'zib'|'zib 2'>&list", array_slice($list, 0, max($limit, 1)));
28+
return array_slice($list, 0, max($limit, 1));
29+
}
30+
31+
public function listVariants(): void
32+
{
33+
$arr = [
34+
2 => 'zib',
35+
4 => 'zib 2',
36+
];
37+
38+
assertType("array{2: 'zib', 4: 'zib 2'}", $arr);
39+
shuffle($arr);
40+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $arr);
41+
42+
$list = [
43+
'zib',
44+
'zib 2',
45+
];
46+
47+
assertType("array{'zib', 'zib 2'}", $list);
48+
shuffle($list);
49+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", $list);
50+
51+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1));
52+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0));
53+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1)); // could be non-empty-array
54+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2));
55+
56+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 1));
57+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 1));
58+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 1)); // could be non-empty-array
59+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 1));
60+
61+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 2));
62+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 2));
63+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 2)); // could be non-empty-array
64+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 2));
65+
66+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3));
67+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3));
68+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3)); // could be non-empty-array
69+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3));
70+
71+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, true));
72+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, true));
73+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, true)); // could be non-empty-array
74+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, true));
75+
76+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, -1, 3, false));
77+
assertType("non-empty-array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 0, 3, false));
78+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 1, 3, false)); // could be non-empty-array
79+
assertType("array<0|1, 'zib'|'zib 2'>&list", array_slice($list, 2, 3, false));
80+
}
81+
82+
/**
83+
* @param array<int, string> $strings
84+
* @param 0|1 $maybeZero
85+
*/
86+
public function arrayVariants(array $strings, $maybeZero): void
87+
{
88+
assertType("array<int, string>", $strings);
89+
assertType("array<int, string>", array_slice($strings, 0));
90+
assertType("array<int, string>", array_slice($strings, 1));
91+
assertType("array<int, string>", array_slice($strings, $maybeZero));
92+
93+
if (count($strings) > 0) {
94+
assertType("non-empty-array<int, string>", $strings);
95+
assertType("non-empty-array<int, string>", array_slice($strings, 0));
96+
assertType("array<int, string>", array_slice($strings, 1));
97+
assertType("array<int, string>", array_slice($strings, $maybeZero));
98+
}
99+
}
100+
}

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,4 +1024,9 @@ public function testArrayPushPreservesList(): void
10241024
$this->analyse([__DIR__ . '/data/array-push-preserves-list.php'], []);
10251025
}
10261026

1027+
public function testBug10721(): void
1028+
{
1029+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-10721.php'], []);
1030+
}
1031+
10271032
}

0 commit comments

Comments
 (0)