Skip to content

Commit 99001bc

Browse files
authored
count(non-empty-array, COUNT_RECURSIVE) is int<1, max>
1 parent 4d834fd commit 99001bc

File tree

4 files changed

+268
-9
lines changed

4 files changed

+268
-9
lines changed

src/Type/IntersectionType.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -680,7 +680,22 @@ public function isIterableAtLeastOnce(): TrinaryLogic
680680

681681
public function getArraySize(): Type
682682
{
683-
return $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
683+
$arraySize = $this->intersectTypes(static fn (Type $type): Type => $type->getArraySize());
684+
685+
$knownOffsets = [];
686+
foreach ($this->types as $type) {
687+
if (!($type instanceof HasOffsetValueType) && !($type instanceof HasOffsetType)) {
688+
continue;
689+
}
690+
691+
$knownOffsets[$type->getOffsetType()->getValue()] = true;
692+
}
693+
694+
if ($knownOffsets !== []) {
695+
return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
696+
}
697+
698+
return $arraySize;
684699
}
685700

686701
public function getIterableKeyType(): Type

src/Type/Php/CountFunctionReturnTypeExtension.php

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\DependencyInjection\AutowiredService;
88
use PHPStan\Reflection\FunctionReflection;
9+
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\Constant\ConstantIntegerType;
1011
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
12+
use PHPStan\Type\IntegerRangeType;
1113
use PHPStan\Type\Type;
1214
use function count;
1315
use function in_array;
14-
use const COUNT_RECURSIVE;
16+
use const COUNT_NORMAL;
1517

1618
#[AutowiredService]
1719
final class CountFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
@@ -28,18 +30,31 @@ public function getTypeFromFunctionCall(
2830
Scope $scope,
2931
): ?Type
3032
{
31-
if (count($functionCall->getArgs()) < 1) {
33+
$args = $functionCall->getArgs();
34+
if (count($args) < 1) {
3235
return null;
3336
}
3437

35-
if (count($functionCall->getArgs()) > 1) {
36-
$mode = $scope->getType($functionCall->getArgs()[1]->value);
37-
if ($mode->isSuperTypeOf(new ConstantIntegerType(COUNT_RECURSIVE))->yes()) {
38-
return null;
38+
$arrayType = $scope->getType($args[0]->value);
39+
if (!$this->isNormalCount($functionCall, $arrayType, $scope)->yes()) {
40+
if ($arrayType->isIterableAtLeastOnce()->yes()) {
41+
return IntegerRangeType::fromInterval(1, null);
3942
}
43+
return null;
4044
}
4145

42-
return $scope->getType($functionCall->getArgs()[0]->value)->getArraySize();
46+
return $scope->getType($args[0]->value)->getArraySize();
47+
}
48+
49+
private function isNormalCount(FuncCall $countFuncCall, Type $countedType, Scope $scope): TrinaryLogic
50+
{
51+
if (count($countFuncCall->getArgs()) === 1) {
52+
$isNormalCount = TrinaryLogic::createYes();
53+
} else {
54+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
55+
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($countedType->getIterableValueType()->isArray()->negate());
56+
}
57+
return $isNormalCount;
4358
}
4459

4560
}

tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2464,7 +2464,7 @@ public static function dataBinaryOperations(): array
24642464
'count($arrayOfIntegers)',
24652465
],
24662466
[
2467-
'int<0, max>',
2467+
'3',
24682468
'count($arrayOfIntegers, \COUNT_RECURSIVE)',
24692469
],
24702470
[
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
<?php
2+
3+
namespace CountRecursive;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param array<array<mixed>> $muliDimArr
11+
* @return void
12+
*/
13+
public function countMultiDim(array $muliDimArr, $mixed): void
14+
{
15+
if (count($muliDimArr, $mixed) > 2) {
16+
assertType('int<1, max>', count($muliDimArr));
17+
assertType('int<3, max>', count($muliDimArr, $mixed));
18+
assertType('int<1, max>', count($muliDimArr, COUNT_NORMAL));
19+
assertType('int<1, max>', count($muliDimArr, COUNT_RECURSIVE));
20+
}
21+
}
22+
23+
public function countUnknownArray(array $arr): void
24+
{
25+
assertType('array', $arr);
26+
assertType('int<0, max>', count($arr));
27+
assertType('int<0, max>', count($arr, COUNT_NORMAL));
28+
assertType('int<0, max>', count($arr, COUNT_RECURSIVE));
29+
}
30+
31+
public function countEmptyArray(array $arr): void
32+
{
33+
if (count($arr) == 0) {
34+
assertType('array{}', $arr);
35+
assertType('0', count($arr));
36+
assertType('0', count($arr, COUNT_NORMAL));
37+
assertType('0', count($arr, COUNT_RECURSIVE));
38+
}
39+
}
40+
41+
public function countArray(array $arr): void
42+
{
43+
if (count($arr) > 2) {
44+
assertType('non-empty-array', $arr);
45+
assertType('int<3, max>', count($arr));
46+
assertType('int<1, max>', count($arr, COUNT_NORMAL)); // could be int<3, max>
47+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE));
48+
}
49+
}
50+
51+
public function countArrayNormal(array $arr): void
52+
{
53+
if (count($arr, COUNT_NORMAL) > 2) {
54+
assertType('non-empty-array', $arr);
55+
assertType('int<1, max>', count($arr)); // could be int<3, max>
56+
assertType('int<3, max>', count($arr, COUNT_NORMAL));
57+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE));
58+
}
59+
}
60+
61+
public function countArrayRecursive(array $arr): void
62+
{
63+
if (count($arr, COUNT_RECURSIVE) > 2) {
64+
assertType('non-empty-array', $arr);
65+
assertType('int<1, max>', count($arr));
66+
assertType('int<1, max>', count($arr, COUNT_NORMAL));
67+
assertType('int<3, max>', count($arr, COUNT_RECURSIVE));
68+
}
69+
}
70+
71+
public function countArrayUnionMode(array $arr): void
72+
{
73+
$mode = rand(0,1) ? COUNT_NORMAL : COUNT_RECURSIVE;
74+
if (count($arr, $mode) > 2) {
75+
assertType('non-empty-array', $arr);
76+
assertType('int<3, max>', count($arr, $mode));
77+
assertType('int<1, max>', count($arr, COUNT_NORMAL));
78+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE));
79+
}
80+
}
81+
82+
/** @param list<int> $list */
83+
public function countList($list): void
84+
{
85+
if (count($list) > 2) {
86+
assertType('int<3, max>', count($list));
87+
assertType('int<1, max>', count($list, COUNT_NORMAL));
88+
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
89+
}
90+
}
91+
92+
/** @param list<int> $list */
93+
public function countListNormal($list): void
94+
{
95+
if (count($list, COUNT_NORMAL) > 2) {
96+
assertType('int<1, max>', count($list));
97+
assertType('int<3, max>', count($list, COUNT_NORMAL));
98+
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
99+
}
100+
}
101+
102+
public function countImplicitNormal($mode): void
103+
{
104+
$arr = [1, 2, 3];
105+
if (count($arr, $mode) > 2) {
106+
assertType('3', count($arr));
107+
assertType('3', count($arr, $mode));
108+
assertType('3', count($arr, COUNT_NORMAL));
109+
assertType('3', count($arr, COUNT_RECURSIVE));
110+
}
111+
}
112+
113+
public function countMixed($arr, $mode): void
114+
{
115+
if (count($arr, $mode) > 2) {
116+
assertType('int<0, max>', count($arr));
117+
assertType('int<3, max>', count($arr, $mode));
118+
assertType('int<0, max>', count($arr, COUNT_NORMAL));
119+
assertType('int<0, max>', count($arr, COUNT_RECURSIVE));
120+
}
121+
}
122+
123+
/** @param list<int> $list */
124+
public function countListRecursive($list): void
125+
{
126+
if (count($list, COUNT_RECURSIVE) > 2) {
127+
assertType('int<1, max>', count($list));
128+
assertType('int<1, max>', count($list, COUNT_NORMAL));
129+
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
130+
}
131+
}
132+
133+
/** @param arary<int> $array */
134+
public function countListRecursiveOnUnionOfRanges($array): void
135+
{
136+
if (!array_key_exists(5, $array)) {
137+
return;
138+
}
139+
assertType('non-empty-array&hasOffset(5)', $array);
140+
assertType('int<1, max>', count($array));
141+
142+
if (
143+
(count($array) > 2 && count($array) < 5)
144+
|| (count($array) > 20 && count($array) < 50)
145+
) {
146+
assertType('int<3, 4>|int<21, 49>', count($array));
147+
}
148+
}
149+
150+
151+
public function countConstantArray(array $anotherArray): void {
152+
$arr = [1, 2, 3, [4, 5]];
153+
assertType('4', count($arr));
154+
assertType('4', count($arr, COUNT_NORMAL));
155+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE));
156+
157+
$arr = [1, 2, 3, $anotherArray];
158+
assertType('array{1, 2, 3, array}', $arr);
159+
assertType('4', count($arr));
160+
assertType('4', count($arr, COUNT_NORMAL));
161+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<4, max>
162+
163+
if (rand(0,1)) {
164+
$arr[] = 10;
165+
}
166+
assertType('array{0: 1, 1: 2, 2: 3, 3: array, 4?: 10}', $arr);
167+
assertType('int<4, 5>', count($arr));
168+
assertType('int<4, 5>', count($arr, COUNT_NORMAL));
169+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<4, max>
170+
171+
$arr = [1, 2, 3] + $anotherArray;
172+
assertType('non-empty-array&hasOffsetValue(0, 1)&hasOffsetValue(1, 2)&hasOffsetValue(2, 3)', $arr);
173+
assertType('int<3, max>', count($arr));
174+
assertType('int<3, max>', count($arr, COUNT_NORMAL));
175+
assertType('int<1, max>', count($arr, COUNT_RECURSIVE)); // could be int<3, max>
176+
}
177+
178+
public function countAfterKeyExists(array $array, int $i): void {
179+
if (array_key_exists(5, $array)) {
180+
assertType('non-empty-array&hasOffset(5)', $array);
181+
assertType('int<1, max>', count($array));
182+
}
183+
184+
if ($array !== []) {
185+
assertType('non-empty-array', $array);
186+
assertType('int<1, max>', count($array));
187+
if (array_key_exists(5, $array)) {
188+
assertType('non-empty-array&hasOffset(5)', $array);
189+
assertType('int<1, max>', count($array));
190+
191+
if (array_key_exists(15, $array)) {
192+
assertType('non-empty-array&hasOffset(15)&hasOffset(5)', $array);
193+
assertType('int<2, max>', count($array));
194+
}
195+
}
196+
}
197+
}
198+
199+
public function unionIntegerCountAfterKeyExists(array $array, int $i): void {
200+
if ($array === []) {
201+
return;
202+
}
203+
204+
assertType('non-empty-array', $array);
205+
if (count($array) === 3 || count($array) === 4) {
206+
assertType('3|4', count($array));
207+
if (array_key_exists(5, $array)) {
208+
assertType('non-empty-array&hasOffset(5)', $array);
209+
assertType('3|4', count($array));
210+
}
211+
}
212+
}
213+
214+
public function countMaybeCountable(array $arr, bool $b, int $i) {
215+
$c = rand(0,1) ? $arr : $b;
216+
assertType('array|bool', $c);
217+
assertType('int<0, max>', count($c, $i));
218+
219+
if ($arr === []) {
220+
return;
221+
}
222+
assertType('int<1, max>', count($arr, $i));
223+
224+
$c = rand(0,1) ? $arr : $b;
225+
assertType('non-empty-array|bool', $c);
226+
assertType('int<0, max>', count($c, $i));
227+
228+
}
229+
}

0 commit comments

Comments
 (0)