Skip to content

Commit 4a99621

Browse files
committed
phpstan-assert with recursive count() results in type loss
Infer constant array type from specifying recursive count() on a list type fix inference with smaller/smaller equal on recursive count() add another test fix fix more tests more tests support recursive count on non-recursive element count recursive based on array type more tests fix test lists fix built support countables more assertions cs
1 parent 193756d commit 4a99621

File tree

7 files changed

+588
-5
lines changed

7 files changed

+588
-5
lines changed

src/Analyser/TypeSpecifier.php

+55-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Analyser;
44

5+
use Countable;
56
use PhpParser\Node;
67
use PhpParser\Node\Expr;
78
use PhpParser\Node\Expr\ArrayDimFetch;
@@ -79,6 +80,7 @@
7980
use function is_string;
8081
use function strtolower;
8182
use function substr;
83+
use const COUNT_NORMAL;
8284

8385
class TypeSpecifier
8486
{
@@ -208,7 +210,7 @@ public function specifyTypesInCondition(
208210

209211
if (
210212
$expr->left instanceof FuncCall
211-
&& count($expr->left->getArgs()) === 1
213+
&& count($expr->left->getArgs()) >= 1
212214
&& $expr->left->name instanceof Name
213215
&& in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen'], true)
214216
&& (
@@ -237,7 +239,7 @@ public function specifyTypesInCondition(
237239
if (
238240
!$context->null()
239241
&& $expr->right instanceof FuncCall
240-
&& count($expr->right->getArgs()) === 1
242+
&& count($expr->right->getArgs()) >= 1
241243
&& $expr->right->name instanceof Name
242244
&& in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true)
243245
&& $leftType->isInteger()->yes()
@@ -247,6 +249,39 @@ public function specifyTypesInCondition(
247249
|| ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes())
248250
) {
249251
$argType = $scope->getType($expr->right->getArgs()[0]->value);
252+
253+
if ($context->truthy() && $argType->isArray()->maybe()) {
254+
$countables = [];
255+
if ($argType instanceof UnionType) {
256+
$countableInterface = new ObjectType(Countable::class);
257+
foreach ($argType->getTypes() as $innerType) {
258+
if (
259+
$innerType->isArray()->yes()
260+
) {
261+
$innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType);
262+
if ($innerType->isList()->yes()) {
263+
$innerType = AccessoryArrayListType::intersectWith($innerType);
264+
}
265+
$countables[] = $innerType;
266+
}
267+
268+
if (
269+
!$countableInterface->isSuperTypeOf($innerType)->yes()
270+
) {
271+
continue;
272+
}
273+
274+
$countables[] = $innerType;
275+
}
276+
}
277+
278+
if (count($countables) > 0) {
279+
$countableType = TypeCombinator::union(...$countables);
280+
281+
return $this->create($expr->right->getArgs()[0]->value, $countableType, $context, false, $scope, $rootExpr);
282+
}
283+
}
284+
250285
if ($argType->isArray()->yes()) {
251286
$newType = new NonEmptyArrayType();
252287
if ($context->true() && $argType->isList()->yes()) {
@@ -944,7 +979,7 @@ private function specifyTypesForConstantBinaryExpression(
944979
if (
945980
!$context->null()
946981
&& $exprNode instanceof FuncCall
947-
&& count($exprNode->getArgs()) === 1
982+
&& count($exprNode->getArgs()) >= 1
948983
&& $exprNode->name instanceof Name
949984
&& in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
950985
&& $constantType instanceof ConstantIntegerType
@@ -954,10 +989,26 @@ private function specifyTypesForConstantBinaryExpression(
954989
if ($constantType->getValue() === 0) {
955990
$newContext = $newContext->negate();
956991
}
992+
957993
$argType = $scope->getType($exprNode->getArgs()[0]->value);
994+
958995
if ($argType->isArray()->yes()) {
996+
if (count($exprNode->getArgs()) === 1) {
997+
$isNormalCount = true;
998+
} else {
999+
$mode = $scope->getType($exprNode->getArgs()[1]->value);
1000+
if (!$mode->isInteger()->yes()) {
1001+
return new SpecifiedTypes();
1002+
}
1003+
1004+
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->yes();
1005+
if (!$isNormalCount) {
1006+
$isNormalCount = $argType->getIterableValueType()->isArray()->no();
1007+
}
1008+
}
1009+
9591010
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
960-
if ($argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1011+
if ($isNormalCount && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
9611012
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
9621013
$itemType = $argType->getIterableValueType();
9631014
for ($i = 0; $i < $constantType->getValue(); $i++) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+1
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public function dataFileAsserts(): iterable
171171
if (PHP_VERSION_ID >= 80000) {
172172
yield from $this->gatherAssertTypes(__DIR__ . '/data/minmax-php8.php');
173173
}
174+
yield from $this->gatherAssertTypes(__DIR__ . '/data/count-maybe.php');
174175
yield from $this->gatherAssertTypes(__DIR__ . '/data/classPhpDocs.php');
175176
yield from $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array-key-type.php');
176177
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-3133.php');

tests/PHPStan/Analyser/data/bug-10264.php

+48-1
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,64 @@ function doFoo() {
1717
assertType('list<Bug10264\A>', $list);
1818
}
1919

20+
function doFoo2() {
21+
/** @var list<A> $list */
22+
$list = [];
23+
24+
assertType('list<Bug10264\A>', $list);
25+
26+
assert((count($list, COUNT_NORMAL) <= 1) === true);
27+
assertType('list<Bug10264\A>', $list);
28+
}
29+
2030
/** @param list<int> $c */
2131
public function sayHello(array $c): void
2232
{
2333
assertType('list<int>', $c);
2434
if (count($c) > 0) {
25-
$c = array_map(fn () => new stdClass(), $c);
35+
$c = array_map(fn() => new stdClass(), $c);
2636
assertType('non-empty-list<stdClass>', $c);
2737
} else {
2838
assertType('array{}', $c);
2939
}
3040

3141
assertType('list<stdClass>', $c);
3242
}
43+
44+
function doBar() {
45+
/** @var list<A> $list */
46+
$list = [];
47+
48+
assertType('list<Bug10264\A>', $list);
49+
50+
assert((count($list, COUNT_RECURSIVE) <= 1) === true);
51+
assertType('list<Bug10264\A>', $list);
52+
}
53+
54+
function doIf():void {
55+
/** @var list<A> $list */
56+
$list = [];
57+
58+
assertType('list<Bug10264\A>', $list);
59+
60+
if( count($list, COUNT_RECURSIVE) >= 1) {
61+
assertType('non-empty-list<Bug10264\A>', $list);
62+
} else {
63+
assertType('array{}', $list);
64+
}
65+
}
66+
67+
function countModeInt(int $i):void {
68+
/** @var list<A> $list */
69+
$list = [];
70+
71+
assertType('list<Bug10264\A>', $list);
72+
73+
if( count($list, $i) >= 1) {
74+
assertType('non-empty-list<Bug10264\A>', $list);
75+
} else {
76+
assertType('array{}', $list);
77+
}
78+
}
79+
3380
}
+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
<?php
2+
3+
namespace CountMaybe;
4+
5+
use Countable;
6+
use function PHPStan\Testing\assertType;
7+
8+
function doBar1(float $notCountable, int $mode): void
9+
{
10+
if (count($notCountable, $mode) > 0) {
11+
assertType('float', $notCountable);
12+
} else {
13+
assertType('float', $notCountable);
14+
}
15+
assertType('float', $notCountable);
16+
}
17+
18+
/**
19+
* @param array|int $maybeMode
20+
*/
21+
function doBar2(float $notCountable, $maybeMode): void
22+
{
23+
if (count($notCountable, $maybeMode) > 0) {
24+
assertType('float', $notCountable);
25+
} else {
26+
assertType('float', $notCountable);
27+
}
28+
assertType('float', $notCountable);
29+
}
30+
31+
function doBar3(float $notCountable, float $invalidMode): void
32+
{
33+
if (count($notCountable, $invalidMode) > 0) {
34+
assertType('float', $notCountable);
35+
} else {
36+
assertType('float', $notCountable);
37+
}
38+
assertType('float', $notCountable);
39+
}
40+
41+
/**
42+
* @param float|int[] $maybeCountable
43+
*/
44+
function doFoo1($maybeCountable, int $mode): void
45+
{
46+
if (count($maybeCountable, $mode) > 0) {
47+
assertType('non-empty-array<int>', $maybeCountable);
48+
} else {
49+
assertType('array<int>|float', $maybeCountable);
50+
}
51+
assertType('array<int>|float', $maybeCountable);
52+
}
53+
54+
/**
55+
* @param float|int[] $maybeCountable
56+
* @param array|int $maybeMode
57+
*/
58+
function doFoo2($maybeCountable, $maybeMode): void
59+
{
60+
if (count($maybeCountable, $maybeMode) > 0) {
61+
assertType('non-empty-array<int>', $maybeCountable);
62+
} else {
63+
assertType('array<int>|float', $maybeCountable);
64+
}
65+
assertType('array<int>|float', $maybeCountable);
66+
}
67+
68+
/**
69+
* @param float|int[] $maybeCountable
70+
*/
71+
function doFoo3($maybeCountable, float $invalidMode): void
72+
{
73+
if (count($maybeCountable, $invalidMode) > 0) {
74+
assertType('non-empty-array<int>', $maybeCountable);
75+
} else {
76+
assertType('array<int>|float', $maybeCountable);
77+
}
78+
assertType('array<int>|float', $maybeCountable);
79+
}
80+
81+
/**
82+
* @param float|list<int> $maybeCountable
83+
*/
84+
function doFoo4($maybeCountable, int $mode): void
85+
{
86+
if (count($maybeCountable, $mode) > 0) {
87+
assertType('non-empty-list<int>', $maybeCountable);
88+
} else {
89+
assertType('list<int>|float', $maybeCountable);
90+
}
91+
assertType('list<int>|float', $maybeCountable);
92+
}
93+
94+
/**
95+
* @param float|list<int> $maybeCountable
96+
* @param array|int $maybeMode
97+
*/
98+
function doFoo5($maybeCountable, $maybeMode): void
99+
{
100+
if (count($maybeCountable, $maybeMode) > 0) {
101+
assertType('non-empty-list<int>', $maybeCountable);
102+
} else {
103+
assertType('list<int>|float', $maybeCountable);
104+
}
105+
assertType('list<int>|float', $maybeCountable);
106+
}
107+
108+
/**
109+
* @param float|list<int> $maybeCountable
110+
*/
111+
function doFoo6($maybeCountable, float $invalidMode): void
112+
{
113+
if (count($maybeCountable, $invalidMode) > 0) {
114+
assertType('non-empty-list<int>', $maybeCountable);
115+
} else {
116+
assertType('list<int>|float', $maybeCountable);
117+
}
118+
assertType('list<int>|float', $maybeCountable);
119+
}
120+
121+
/**
122+
* @param float|list<int>|Countable $maybeCountable
123+
*/
124+
function doFoo7($maybeCountable, int $mode): void
125+
{
126+
if (count($maybeCountable, $mode) > 0) {
127+
assertType('non-empty-list<int>|Countable', $maybeCountable);
128+
} else {
129+
assertType('list<int>|Countable|float', $maybeCountable);
130+
}
131+
assertType('list<int>|Countable|float', $maybeCountable);
132+
}
133+
134+
/**
135+
* @param float|list<int>|Countable $maybeCountable
136+
* @param array|int $maybeMode
137+
*/
138+
function doFoo8($maybeCountable, $maybeMode): void
139+
{
140+
if (count($maybeCountable, $maybeMode) > 0) {
141+
assertType('non-empty-list<int>|Countable', $maybeCountable);
142+
} else {
143+
assertType('list<int>|Countable|float', $maybeCountable);
144+
}
145+
assertType('list<int>|Countable|float', $maybeCountable);
146+
}
147+
148+
/**
149+
* @param float|list<int>|Countable $maybeCountable
150+
*/
151+
function doFoo9($maybeCountable, float $invalidMode): void
152+
{
153+
if (count($maybeCountable, $invalidMode) > 0) {
154+
assertType('non-empty-list<int>|Countable', $maybeCountable);
155+
} else {
156+
assertType('list<int>|Countable|float', $maybeCountable);
157+
}
158+
assertType('list<int>|Countable|float', $maybeCountable);
159+
}
160+
161+
function doFooBar1(array $countable, int $mode): void
162+
{
163+
if (count($countable, $mode) > 0) {
164+
assertType('non-empty-array', $countable);
165+
} else {
166+
assertType('array{}', $countable);
167+
}
168+
assertType('array', $countable);
169+
}
170+
171+
/**
172+
* @param array|int $maybeMode
173+
*/
174+
function doFooBar2(array $countable, $maybeMode): void
175+
{
176+
if (count($countable, $maybeMode) > 0) {
177+
assertType('non-empty-array', $countable);
178+
} else {
179+
assertType('array{}', $countable);
180+
}
181+
assertType('array', $countable);
182+
}
183+
184+
function doFooBar3(array $countable, float $invalidMode): void
185+
{
186+
if (count($countable, $invalidMode) > 0) {
187+
assertType('non-empty-array', $countable);
188+
} else {
189+
assertType('array{}', $countable);
190+
}
191+
assertType('array', $countable);
192+
}

0 commit comments

Comments
 (0)