Skip to content

Commit 6ab2ac2

Browse files
committed
Improve count on list with greater/smaller-than
1 parent 8ac3a91 commit 6ab2ac2

File tree

6 files changed

+155
-11
lines changed

6 files changed

+155
-11
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
4141
use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
4242
use PHPStan\Type\Accessory\HasOffsetType;
43+
use PHPStan\Type\Accessory\HasOffsetValueType;
4344
use PHPStan\Type\Accessory\HasPropertyType;
4445
use PHPStan\Type\Accessory\NonEmptyArrayType;
4546
use PHPStan\Type\ArrayType;
@@ -95,6 +96,8 @@
9596
final class TypeSpecifier
9697
{
9798

99+
private const MAX_ACCESSORIES_LIMIT = 8;
100+
98101
/** @var MethodTypeSpecifyingExtension[][]|null */
99102
private ?array $methodTypeSpecifyingExtensionsByClass = null;
100103

@@ -1217,7 +1220,27 @@ private function specifyTypesForCountFuncCall(
12171220
$builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()];
12181221
}
12191222
} else {
1220-
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1223+
$intersection = [];
1224+
$intersection[] = $arrayType;
1225+
$intersection[] = new NonEmptyArrayType();
1226+
1227+
$zero = new ConstantIntegerType(0);
1228+
$i = 0;
1229+
foreach ($builderData as [$offsetType, $valueType]) {
1230+
// non-empty-list already implies the offset 0
1231+
if ($zero->isSuperTypeOf($offsetType)->yes()) {
1232+
continue;
1233+
}
1234+
1235+
if ($i > self::MAX_ACCESSORIES_LIMIT) {
1236+
break;
1237+
}
1238+
1239+
$intersection[] = new HasOffsetValueType($offsetType, $valueType);
1240+
$i++;
1241+
}
1242+
1243+
$resultTypes[] = TypeCombinator::intersect(...$intersection);
12211244
continue;
12221245
}
12231246

src/Type/IntersectionType.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,10 @@ public function getArraySize(): Type
714714
$knownOffsets[$type->getOffsetType()->getValue()] = true;
715715
}
716716

717+
if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
718+
$knownOffsets[0] = true;
719+
}
720+
717721
if ($knownOffsets !== []) {
718722
return TypeCombinator::intersect($arraySize, IntegerRangeType::fromInterval(count($knownOffsets), null));
719723
}
@@ -853,9 +857,26 @@ public function isOffsetAccessLegal(): TrinaryLogic
853857

854858
public function hasOffsetValueType(Type $offsetType): TrinaryLogic
855859
{
856-
if ($this->isList()->yes() && $this->isIterableAtLeastOnce()->yes()) {
860+
if ($this->isList()->yes()) {
857861
$arrayKeyOffsetType = $offsetType->toArrayKey();
858-
if ((new ConstantIntegerType(0))->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
862+
863+
$negative = IntegerRangeType::fromInterval(null, -1);
864+
if ($negative->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
865+
return TrinaryLogic::createNo();
866+
}
867+
868+
$size = $this->getArraySize();
869+
if ($size instanceof IntegerRangeType && $size->getMin() !== null) {
870+
$knownOffsets = IntegerRangeType::fromInterval(0, $size->getMin() - 1);
871+
} elseif ($size instanceof ConstantIntegerType) {
872+
$knownOffsets = IntegerRangeType::fromInterval(0, $size->getValue() - 1);
873+
} elseif ($this->isIterableAtLeastOnce()->yes()) {
874+
$knownOffsets = new ConstantIntegerType(0);
875+
} else {
876+
$knownOffsets = null;
877+
}
878+
879+
if ($knownOffsets !== null && $knownOffsets->isSuperTypeOf($arrayKeyOffsetType)->yes()) {
859880
return TrinaryLogic::createYes();
860881
}
861882

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ function doFoo() {
3434
if (count($entries) !== count($payload->ids)) {
3535
exit();
3636
}
37+
assertType('int<1, max>', count($entries));
3738

3839
assertType('non-empty-list<stdClass>', $entries);
3940
if (count($entries) > 3) {
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13747;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/** @param list<int> $list */
10+
public function count($list): void
11+
{
12+
if (count($list) === 0) {
13+
return;
14+
}
15+
16+
if (count($list) > 2) {
17+
assertType('false', array_key_exists(-1, $list));
18+
assertType('true', array_key_exists(0, $list));
19+
assertType('true', array_key_exists(1, $list));
20+
assertType('true', array_key_exists(2, $list));
21+
assertType('bool', array_key_exists(3, $list));
22+
23+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list);
24+
assertType('int<3, max>', count($list));
25+
} else {
26+
assertType('non-empty-list<int>', $list);
27+
}
28+
assertType('non-empty-list<int>', $list);
29+
30+
if (count($list, COUNT_NORMAL) > 2) {
31+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list);
32+
assertType('int<3, max>', count($list, COUNT_NORMAL));
33+
} else {
34+
assertType('non-empty-list<int>', $list);
35+
}
36+
37+
assertType('non-empty-list<int>', $list);
38+
if (count($list, COUNT_RECURSIVE) > 2) { // COUNT_RECURSIVE on non-recursive array
39+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list);
40+
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
41+
} else {
42+
assertType('non-empty-list<int>', $list);
43+
}
44+
}
45+
46+
/** @param list<int> $list */
47+
public function doFoo($list): void
48+
{
49+
if (count($list) === 0) {
50+
return;
51+
}
52+
53+
if (count($list) >= 2) {
54+
assertType('non-empty-list<int>&hasOffsetValue(1, int)', $list);
55+
assertType('int<2, max>', count($list));
56+
} else {
57+
assertType('non-empty-list<int>', $list);
58+
}
59+
}
60+
61+
/** @param list<int> $list */
62+
public function doBar($list): void
63+
{
64+
if (count($list) === 0) {
65+
return;
66+
}
67+
68+
if (2 <= count($list)) {
69+
assertType('non-empty-list<int>&hasOffsetValue(1, int)', $list);
70+
assertType('int<2, max>', count($list));
71+
} else {
72+
assertType('non-empty-list<int>', $list);
73+
assertType('1', count($list));
74+
}
75+
}
76+
77+
/** @param list<int> $list */
78+
public function checkLimit($list): void
79+
{
80+
if (count($list) === 0) {
81+
return;
82+
}
83+
84+
if (count($list) > 9) {
85+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)&hasOffsetValue(3, int)&hasOffsetValue(4, int)&hasOffsetValue(5, int)&hasOffsetValue(6, int)&hasOffsetValue(7, int)&hasOffsetValue(8, int)&hasOffsetValue(9, int)', $list);
86+
assertType('int<10, max>', count($list));
87+
} else {
88+
assertType('non-empty-list<int>', $list);
89+
}
90+
91+
if (count($list) > 10) {
92+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)&hasOffsetValue(3, int)&hasOffsetValue(4, int)&hasOffsetValue(5, int)&hasOffsetValue(6, int)&hasOffsetValue(7, int)&hasOffsetValue(8, int)&hasOffsetValue(9, int)', $list);
93+
assertType('int<11, max>', count($list));
94+
} else {
95+
assertType('non-empty-list<int>', $list);
96+
}
97+
98+
}
99+
}

tests/PHPStan/Analyser/nsrt/count-recursive.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,18 +84,18 @@ public function countList($list): void
8484
{
8585
if (count($list) > 2) {
8686
assertType('int<3, max>', count($list));
87-
assertType('int<1, max>', count($list, COUNT_NORMAL));
88-
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
87+
assertType('int<3, max>', count($list, COUNT_NORMAL));
88+
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
8989
}
9090
}
9191

9292
/** @param list<int> $list */
9393
public function countListNormal($list): void
9494
{
9595
if (count($list, COUNT_NORMAL) > 2) {
96-
assertType('int<1, max>', count($list));
96+
assertType('int<3, max>', count($list));
9797
assertType('int<3, max>', count($list, COUNT_NORMAL));
98-
assertType('int<1, max>', count($list, COUNT_RECURSIVE));
98+
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
9999
}
100100
}
101101

@@ -124,8 +124,8 @@ public function countMixed($arr, $mode): void
124124
public function countListRecursive($list): void
125125
{
126126
if (count($list, COUNT_RECURSIVE) > 2) {
127-
assertType('int<1, max>', count($list));
128-
assertType('int<1, max>', count($list, COUNT_NORMAL));
127+
assertType('int<3, max>', count($list));
128+
assertType('int<3, max>', count($list, COUNT_NORMAL));
129129
assertType('int<3, max>', count($list, COUNT_RECURSIVE));
130130
}
131131
}

tests/PHPStan/Analyser/nsrt/list-count2.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,10 @@ function skipRecursiveRightCount($arrA, array $arrB): void
201201

202202
if (count($arrA) == count($arrB, COUNT_RECURSIVE)) {
203203
assertType('array{int, int, int}', $arrA);
204-
assertType('non-empty-list', $arrB);
204+
assertType('non-empty-list&hasOffsetValue(1, mixed)', $arrB);
205205
}
206206
assertType('array{int, int, int}', $arrA);
207-
assertType('non-empty-list', $arrB);
207+
assertType('non-empty-list&hasOffsetValue(1, mixed)', $arrB);
208208
}
209209

210210
/**

0 commit comments

Comments
 (0)