Skip to content

Commit 411c411

Browse files
authored
Merge branch refs/heads/2.1.x into 2.2.x
2 parents cbba989 + 5557c78 commit 411c411

File tree

6 files changed

+283
-11
lines changed

6 files changed

+283
-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: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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+
if (count($list) < 5) {
61+
assertType('non-empty-list<int>', $list);
62+
assertType('int<1, 4>', count($list));
63+
} else {
64+
assertType('non-empty-list<int>&hasOffsetValue(1, int)&hasOffsetValue(2, int)&hasOffsetValue(3, int)&hasOffsetValue(4, int)', $list);
65+
}
66+
}
67+
68+
/** @param list<int> $list */
69+
public function doBar($list): void
70+
{
71+
if (count($list) === 0) {
72+
return;
73+
}
74+
75+
if (2 <= count($list)) {
76+
assertType('non-empty-list<int>&hasOffsetValue(1, int)', $list);
77+
assertType('int<2, max>', count($list));
78+
} else {
79+
assertType('non-empty-list<int>', $list);
80+
assertType('1', count($list));
81+
}
82+
}
83+
84+
/** @param non-empty-array<int> $nonEmptyArray */
85+
public function doNonEmptyArray($nonEmptyArray): void
86+
{
87+
if (2 <= count($nonEmptyArray)) {
88+
assertType('non-empty-array<int>', $nonEmptyArray);
89+
assertType('int<2, max>', count($nonEmptyArray));
90+
} else {
91+
assertType('non-empty-array<int>', $nonEmptyArray);
92+
assertType('1', count($nonEmptyArray));
93+
}
94+
95+
if (count($nonEmptyArray) < 5) {
96+
assertType('non-empty-array<int>', $nonEmptyArray);
97+
assertType('int<1, 4>', count($nonEmptyArray));
98+
} else {
99+
assertType('non-empty-array<int>', $nonEmptyArray);
100+
}
101+
}
102+
103+
/**
104+
* @param list<int> $listA
105+
* @param non-empty-list<string> $listB
106+
*/
107+
public function doMaybeBar($listA, $listB): void
108+
{
109+
if (rand(0,1)) {
110+
$list = $listA;
111+
} else {
112+
$list = $listB;
113+
}
114+
115+
if (2 <= count($list)) {
116+
assertType('non-empty-list<int|string>&hasOffsetValue(1, int|string)', $list);
117+
assertType('int<2, max>', count($list));
118+
} else {
119+
assertType('list<int|string>', $list);
120+
assertType('int<0, 1>', count($list));
121+
}
122+
}
123+
124+
/**
125+
* @param array<int> $aArray
126+
* @param non-empty-list<string> $aList
127+
*/
128+
public function doMaybeArray($aArray, $aList): void
129+
{
130+
if (rand(0,1)) {
131+
$listOrArray = $aArray;
132+
} else {
133+
$listOrArray = $aList;
134+
}
135+
136+
if (2 <= count($listOrArray)) {
137+
assertType('non-empty-array<int|string>', $listOrArray);
138+
assertType('int<2, max>', count($listOrArray));
139+
} else {
140+
assertType('array<int|string>', $listOrArray);
141+
assertType('int<0, 1>', count($listOrArray));
142+
}
143+
}
144+
145+
/** @param list<int> $list */
146+
public function doMaybeEmpty($list): void
147+
{
148+
if (rand(0,1)) {
149+
$list = [1, 2, 3];
150+
}
151+
152+
if (2 <= count($list)) {
153+
assertType('non-empty-list<int>&hasOffsetValue(1, int)', $list);
154+
assertType('int<2, max>', count($list));
155+
} else {
156+
assertType('list<int>', $list);
157+
assertType('int<0, 1>', count($list));
158+
}
159+
}
160+
161+
/** @param list<int> $list */
162+
public function checkLimit($list): void
163+
{
164+
if (count($list) === 0) {
165+
return;
166+
}
167+
168+
if (count($list) > 9) {
169+
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);
170+
assertType('int<10, max>', count($list));
171+
} else {
172+
assertType('non-empty-list<int>', $list);
173+
}
174+
175+
if (count($list) > 10) {
176+
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);
177+
assertType('int<11, max>', count($list));
178+
} else {
179+
assertType('non-empty-list<int>', $list);
180+
}
181+
182+
}
183+
184+
/** @param array<int> $array */
185+
public function countArray($array): void
186+
{
187+
if (count($array) < 5) {
188+
assertType('array<int>', $array);
189+
assertType('int<0, 4>', count($array));
190+
} else {
191+
assertType('non-empty-array<int>', $array);
192+
}
193+
194+
if (count($array) === 0) {
195+
return;
196+
}
197+
198+
if (count($array) > 2) {
199+
assertType('bool', array_key_exists(-1, $array));
200+
assertType('bool', array_key_exists(0, $array));
201+
assertType('bool', array_key_exists(1, $array));
202+
assertType('bool', array_key_exists(2, $array));
203+
assertType('bool', array_key_exists(3, $array));
204+
205+
assertType('non-empty-array<int>', $array);
206+
assertType('int<3, max>', count($array));
207+
} else {
208+
assertType('non-empty-array<int>', $array);
209+
}
210+
assertType('non-empty-array<int>', $array);
211+
212+
if (count($array, COUNT_NORMAL) > 2) {
213+
assertType('non-empty-array<int>', $array);
214+
assertType('int<3, max>', count($array, COUNT_NORMAL));
215+
} else {
216+
assertType('non-empty-array<int>', $array);
217+
}
218+
219+
assertType('non-empty-array<int>', $array);
220+
if (count($array, COUNT_RECURSIVE) > 2) { // COUNT_RECURSIVE on non-recursive array
221+
assertType('non-empty-array<int>', $array);
222+
assertType('int<3, max>', count($array, COUNT_RECURSIVE));
223+
} else {
224+
assertType('non-empty-array<int>', $array);
225+
}
226+
}
227+
}

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)