Skip to content

Commit 67fb192

Browse files
committed
More precise count($array) after array_* functions
1 parent 7489a09 commit 67fb192

File tree

3 files changed

+114
-5
lines changed

3 files changed

+114
-5
lines changed

src/Analyser/NodeScopeResolver.php

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
use PhpParser\Node\Expr\Variable;
3939
use PhpParser\Node\Identifier;
4040
use PhpParser\Node\Name;
41+
use PhpParser\Node\Scalar\Int_;
4142
use PhpParser\Node\Stmt\Break_;
4243
use PhpParser\Node\Stmt\Class_;
4344
use PhpParser\Node\Stmt\Continue_;
@@ -174,6 +175,7 @@
174175
use PHPStan\Type\Generic\TemplateTypeMap;
175176
use PHPStan\Type\Generic\TemplateTypeVariance;
176177
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
178+
use PHPStan\Type\IntegerRangeType;
177179
use PHPStan\Type\IntegerType;
178180
use PHPStan\Type\IntersectionType;
179181
use PHPStan\Type\MixedType;
@@ -2628,29 +2630,72 @@ static function (): void {
26282630
&& in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true)
26292631
&& count($expr->getArgs()) >= 1
26302632
) {
2631-
$arrayArg = $expr->getArgs()[0]->value;
2633+
$args = $expr->getArgs();
2634+
$arrayArg = $args[0]->value;
26322635

26332636
$arrayArgType = $scope->getType($arrayArg);
26342637
$arrayArgNativeType = $scope->getNativeType($arrayArg);
26352638

2639+
$countArrayExpr = new FuncCall(new Name('count'), [$args[0]]);
2640+
$hasCountExpr = $scope->hasExpressionType($countArrayExpr)->yes();
2641+
if ($hasCountExpr) {
2642+
$countType = $scope->getType(new BinaryOp\Minus($countArrayExpr, new Int_(1)));
2643+
$countNativeType = $scope->getType(new BinaryOp\Minus($countArrayExpr, new Int_(1)));
2644+
}
2645+
26362646
$isArrayPop = $functionReflection->getName() === 'array_pop';
26372647
$scope = $scope->invalidateExpression($arrayArg)->assignExpression(
26382648
$arrayArg,
26392649
$isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
26402650
$isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
26412651
);
2652+
2653+
if (
2654+
$hasCountExpr
2655+
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($countType)->yes()
2656+
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($countNativeType)->yes()
2657+
) {
2658+
$scope = $scope->assignExpression($countArrayExpr, $countType, $countNativeType);
2659+
}
26422660
}
26432661

26442662
if (
26452663
$functionReflection !== null
26462664
&& in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true)
26472665
&& count($expr->getArgs()) >= 2
26482666
) {
2667+
$args = $expr->getArgs();
26492668
$arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr);
26502669
$arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr);
26512670

2652-
$arrayArg = $expr->getArgs()[0]->value;
2671+
$arrayArg = $args[0]->value;
2672+
$addedElementsCount = count($args) - 1;
2673+
for ($i = 1; $i < count($args); $i++) {
2674+
if ($args[$i]->unpack) {
2675+
$addedElementsCount = null;
2676+
break;
2677+
}
2678+
}
2679+
2680+
$countArrayExpr = new FuncCall(new Name('count'), [$args[0]]);
2681+
$hasCountExpr = $scope->hasExpressionType($countArrayExpr)->yes();
2682+
if ($hasCountExpr && $addedElementsCount !== null) {
2683+
$countType = $scope->getType(new BinaryOp\Plus($countArrayExpr, new Int_($addedElementsCount)));
2684+
$countNativeType = $scope->getType(new BinaryOp\Plus($countArrayExpr, new Int_($addedElementsCount)));
2685+
} else {
2686+
$countType = IntegerRangeType::fromInterval($addedElementsCount, null);
2687+
$countNativeType = IntegerRangeType::fromInterval($addedElementsCount, null);
2688+
}
2689+
26532690
$scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType);
2691+
2692+
if (
2693+
IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($countType)->yes()
2694+
&& IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($countNativeType)->yes()
2695+
) {
2696+
$scope = $scope->assignExpression($countArrayExpr, $countType, $countNativeType);
2697+
}
2698+
26542699
}
26552700

26562701
if (

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ function (array $input) {
1818
\assert(count($input) > 0);
1919
assertType('int<1, max>', count($input));
2020
array_unshift($input, 'test');
21-
assertType('int<1, max>', count($input));
21+
assertType('int<2, max>', count($input));
2222

2323
\assert(count($input) > 0);
24-
assertType('int<1, max>', count($input));
24+
assertType('int<2, max>', count($input));
2525
array_push($input, 'nope');
26-
assertType('int<1, max>', count($input));
26+
assertType('int<3, max>', count($input));
2727
};
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace Bug7804;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/** @param array<int, string> $headers */
8+
function headers(array $headers): void
9+
{
10+
assertType('int<0, max>', count($headers));
11+
if (count($headers) >= 4) {
12+
assertType('int<4, max>', count($headers));
13+
array_pop($headers);
14+
assertType('int<3, max>', count($headers));
15+
array_pop($headers);
16+
assertType('int<2, max>', count($headers));
17+
array_pop($headers);
18+
assertType('int<1, max>', count($headers));
19+
array_pop($headers);
20+
assertType('int<0, max>', count($headers));
21+
array_pop($headers);
22+
assertType('int<0, max>', count($headers));
23+
}
24+
assertType('int<0, max>', count($headers));
25+
}
26+
27+
function doPop(array $arr) {
28+
assertType('int<0, max>', count($arr));
29+
array_pop($arr);
30+
assertType('int<0, max>', count($arr));
31+
32+
if (count($arr) === 2) {
33+
assertType('2', count($arr));
34+
array_pop($arr);
35+
assertType('1', count($arr));
36+
}
37+
assertType('int<0, 1>|int<3, max>', count($arr));
38+
}
39+
40+
function doShift(array $arr) {
41+
assertType('int<0, max>', count($arr));
42+
array_shift($arr);
43+
assertType('int<0, max>', count($arr));
44+
}
45+
46+
function doPush(array $arr, int $i) {
47+
assertType('int<0, max>', count($arr));
48+
array_push($arr, $i);
49+
assertType('int<1, max>', count($arr));
50+
array_push($arr, 3, $i, false, null);
51+
assertType('int<5, max>', count($arr));
52+
}
53+
54+
function doPushVariadic(array $arr, array $arr2) {
55+
assertType('int<0, max>', count($arr));
56+
array_push($arr, ...$arr2);
57+
assertType('int<0, max>', count($arr));
58+
}
59+
60+
function doUnshift(array $arr, bool $b) {
61+
assertType('int<0, max>', count($arr));
62+
array_unshift($arr, $b);
63+
assertType('int<1, max>', count($arr));
64+
}

0 commit comments

Comments
 (0)