Skip to content

Commit 8c1d275

Browse files
authored
Infer non-empty-ness after count($a) == count($b) (#4470)
1 parent c72d43a commit 8c1d275

File tree

2 files changed

+256
-1
lines changed

2 files changed

+256
-1
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2297,7 +2297,7 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
22972297

22982298
$rightType = $scope->getType($rightExpr);
22992299

2300-
// (count($a) === $b)
2300+
// (count($a) === $expr)
23012301
if (
23022302
!$context->null()
23032303
&& $unwrappedLeftExpr instanceof FuncCall
@@ -2306,6 +2306,37 @@ private function resolveNormalizedIdentical(Expr\BinaryOp\Identical $expr, Scope
23062306
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
23072307
&& $rightType->isInteger()->yes()
23082308
) {
2309+
// count($a) === count($b)
2310+
if (
2311+
$context->true()
2312+
&& $unwrappedRightExpr instanceof FuncCall
2313+
&& $unwrappedRightExpr->name instanceof Name
2314+
&& in_array($unwrappedRightExpr->name->toLowerString(), ['count', 'sizeof'], true)
2315+
&& count($unwrappedRightExpr->getArgs()) >= 1
2316+
) {
2317+
$argType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value);
2318+
$sizeType = $scope->getType($leftExpr);
2319+
2320+
$specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedRightExpr, $argType, $sizeType, $context, $scope, $expr);
2321+
if ($specifiedTypes !== null) {
2322+
return $specifiedTypes;
2323+
}
2324+
2325+
$leftArrayType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2326+
$rightArrayType = $scope->getType($unwrappedRightExpr->getArgs()[0]->value);
2327+
if (
2328+
$leftArrayType->isArray()->yes()
2329+
&& $rightArrayType->isArray()->yes()
2330+
&& !$rightType->isConstantScalarValue()->yes()
2331+
&& ($leftArrayType->isIterableAtLeastOnce()->yes() || $rightArrayType->isIterableAtLeastOnce()->yes())
2332+
) {
2333+
$arrayTypes = $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr);
2334+
return $arrayTypes->unionWith(
2335+
$this->create($unwrappedRightExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
2336+
);
2337+
}
2338+
}
2339+
23092340
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
23102341
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
23112342
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<?php
2+
3+
namespace ListCount2;
4+
5+
use function PHPStan\dumpType;
6+
use function PHPStan\debugScope;
7+
use function PHPStan\Testing\assertType;
8+
9+
class HelloWorld
10+
{
11+
/**
12+
* @param non-empty-list<int> $listA
13+
* @param list<int> $listB
14+
*/
15+
public function sayIdenticalLists($listA, array $listB): void
16+
{
17+
if (count($listA) === count($listB)) {
18+
assertType('non-empty-list<int>', $listA);
19+
assertType('non-empty-list<int>', $listB);
20+
}
21+
assertType('non-empty-list<int>', $listA);
22+
assertType('list<int>', $listB);
23+
}
24+
25+
/**
26+
* @param non-empty-list<int> $listA
27+
*/
28+
public function sayIdenticalList($listA, array $arrB): void
29+
{
30+
if (count($listA) === count($arrB)) {
31+
assertType('non-empty-list<int>', $listA);
32+
assertType('non-empty-array', $arrB);
33+
}
34+
assertType('non-empty-list<int>', $listA);
35+
assertType('array', $arrB);
36+
}
37+
38+
/**
39+
* @param non-empty-array<int> $arrA
40+
*/
41+
public function sayEqualArray($arrA, array $arrB): void
42+
{
43+
if (count($arrA) == count($arrB)) {
44+
assertType('non-empty-array<int>', $arrA);
45+
assertType('non-empty-array', $arrB);
46+
}
47+
assertType('non-empty-array<int>', $arrA);
48+
assertType('array', $arrB);
49+
}
50+
51+
/**
52+
* @param non-empty-array<int> $arrA
53+
* @param array<int> $arrB
54+
*/
55+
public function sayEqualIntArray($arrA, array $arrB): void
56+
{
57+
if (count($arrA) == count($arrB)) {
58+
assertType('non-empty-array<int>', $arrA);
59+
assertType('non-empty-array<int>', $arrB);
60+
}
61+
assertType('non-empty-array<int>', $arrA);
62+
assertType('array<int>', $arrB);
63+
}
64+
65+
/**
66+
* @param non-empty-array<int> $arrA
67+
* @param array<string> $arrB
68+
*/
69+
public function sayEqualStringArray($arrA, array $arrB): void
70+
{
71+
if (count($arrA) == count($arrB)) {
72+
assertType('non-empty-array<int>', $arrA);
73+
assertType('non-empty-array<string>', $arrB);
74+
}
75+
assertType('non-empty-array<int>', $arrA);
76+
assertType('array<string>', $arrB);
77+
}
78+
79+
/**
80+
* @param array<int> $arrA
81+
* @param array<string> $arrB
82+
*/
83+
public function sayUnknownSizeArray($arrA, array $arrB): void
84+
{
85+
if (count($arrA) == count($arrB)) {
86+
assertType('array<int>', $arrA);
87+
assertType('array<string>', $arrB);
88+
}
89+
assertType('array<int>', $arrA);
90+
assertType('array<string>', $arrB);
91+
}
92+
93+
/**
94+
* @param array{int, int, int} $arrA
95+
* @param list $arrB
96+
*/
97+
function sayEqualArrayShape($arrA, array $arrB): void
98+
{
99+
if (count($arrA) == count($arrB)) {
100+
assertType('array{int, int, int}', $arrA);
101+
assertType('array{mixed, mixed, mixed}', $arrB);
102+
}
103+
assertType('array{int, int, int}', $arrA);
104+
assertType('list', $arrB);
105+
}
106+
107+
/**
108+
* @param list $arrA
109+
* @param array{int, int, int} $arrB
110+
*/
111+
function sayEqualArrayShapeReversed($arrA, array $arrB): void
112+
{
113+
if (count($arrA) == count($arrB)) {
114+
assertType('array{mixed, mixed, mixed}', $arrA);
115+
assertType('array{int, int, int}', $arrB);
116+
}
117+
assertType('list', $arrA);
118+
assertType('array{int, int, int}', $arrB);
119+
}
120+
121+
/**
122+
* @param array{int, int, int} $arrA
123+
* @param list $arrB
124+
*/
125+
function sayEqualArrayShapeAfterNarrowedCount($arrA, array $arrB): void
126+
{
127+
if (count($arrB) < 2) {
128+
return;
129+
}
130+
131+
if (count($arrA) == count($arrB)) {
132+
assertType('array{int, int, int}', $arrA);
133+
assertType('array{mixed, mixed, mixed}', $arrB);
134+
}
135+
assertType('array{int, int, int}', $arrA);
136+
assertType('non-empty-list', $arrB);
137+
}
138+
139+
/**
140+
* @param non-empty-array $arrB
141+
*/
142+
function dontNarrowEmpty(array $arrB): void
143+
{
144+
$arrA = [];
145+
assertType('array{}', $arrA);
146+
147+
if (count($arrA) == count($arrB)) {
148+
assertType('*NEVER*', $arrA);
149+
assertType('non-empty-array', $arrB); // could be '*NEVER*'
150+
}
151+
assertType('array{}', $arrA);
152+
153+
if (count($arrB) == count($arrA)) {
154+
assertType('*NEVER*', $arrA);
155+
assertType('non-empty-array', $arrB); // could be '*NEVER*'
156+
}
157+
assertType('array{}', $arrA);
158+
}
159+
160+
/**
161+
* @param non-empty-list<int> $listA
162+
* @param list<int> $listB
163+
*/
164+
public function supportsNormalCount($listA, array $listB): void
165+
{
166+
if (count($listA, COUNT_NORMAL) === count($listB)) {
167+
assertType('non-empty-list<int>', $listA);
168+
assertType('non-empty-list<int>', $listB);
169+
}
170+
assertType('non-empty-list<int>', $listA);
171+
assertType('list<int>', $listB);
172+
}
173+
174+
/**
175+
* @param array{int, int, int} $arrA
176+
* @param list $arrB
177+
*/
178+
function skipRecursiveLeftCount($arrA, array $arrB): void
179+
{
180+
if (count($arrB) < 2) {
181+
return;
182+
}
183+
184+
if (count($arrA, COUNT_RECURSIVE) == count($arrB)) {
185+
assertType('array{int, int, int}', $arrA);
186+
assertType('array{mixed, mixed, mixed}', $arrB);
187+
}
188+
assertType('array{int, int, int}', $arrA);
189+
assertType('non-empty-list', $arrB);
190+
}
191+
192+
/**
193+
* @param array{int, int, int} $arrA
194+
* @param list $arrB
195+
*/
196+
function skipRecursiveRightCount($arrA, array $arrB): void
197+
{
198+
if (count($arrB) < 2) {
199+
return;
200+
}
201+
202+
if (count($arrA) == count($arrB, COUNT_RECURSIVE)) {
203+
assertType('array{int, int, int}', $arrA);
204+
assertType('non-empty-list', $arrB);
205+
}
206+
assertType('array{int, int, int}', $arrA);
207+
assertType('non-empty-list', $arrB);
208+
}
209+
210+
/**
211+
* @param non-empty-array<int> $arrA
212+
* @param array<int> $arrB
213+
*/
214+
public function skipRecursiveCount($arrA, array $arrB): void
215+
{
216+
if (count($arrA, COUNT_RECURSIVE) == count($arrB)) {
217+
assertType('non-empty-array<int>', $arrA);
218+
assertType('non-empty-array<int>', $arrB);
219+
}
220+
assertType('non-empty-array<int>', $arrA);
221+
assertType('array<int>', $arrB);
222+
}
223+
224+
}

0 commit comments

Comments
 (0)