Skip to content

Commit 5379e31

Browse files
staabmondrejmirtes
authored andcommitted
Narrow array on count() with positive-int
1 parent 008f65e commit 5379e31

File tree

4 files changed

+109
-53
lines changed

4 files changed

+109
-53
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
use PHPStan\Type\ArrayType;
4040
use PHPStan\Type\BooleanType;
4141
use PHPStan\Type\ConditionalTypeForParameter;
42+
use PHPStan\Type\Constant\ConstantArrayType;
4243
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
4344
use PHPStan\Type\Constant\ConstantBooleanType;
4445
use PHPStan\Type\Constant\ConstantIntegerType;
@@ -1049,7 +1050,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10491050
$offsetType = new ConstantIntegerType($i);
10501051
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true);
10511052
}
1052-
} else {
1053+
} elseif ($type->isConstantArray()->yes()) {
10531054
for ($i = $sizeType->getMin();; $i++) {
10541055
$offsetType = new ConstantIntegerType($i);
10551056
$hasOffset = $type->hasOffsetValueType($offsetType);
@@ -1060,7 +1061,11 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10601061
}
10611062

10621063
}
1063-
return $valueTypesBuilder->getArray();
1064+
1065+
$arrayType = $valueTypesBuilder->getArray();
1066+
if ($arrayType->isIterableAtLeastOnce()->yes()) {
1067+
return $arrayType;
1068+
}
10641069
}
10651070

10661071
return null;
@@ -1102,54 +1107,6 @@ private function specifyTypesForConstantBinaryExpression(
11021107
));
11031108
}
11041109

1105-
if (
1106-
!$context->null()
1107-
&& $exprNode instanceof FuncCall
1108-
&& count($exprNode->getArgs()) >= 1
1109-
&& $exprNode->name instanceof Name
1110-
&& in_array(strtolower((string) $exprNode->name), ['count', 'sizeof'], true)
1111-
&& $constantType instanceof ConstantIntegerType
1112-
) {
1113-
if ($constantType->getValue() < 0) {
1114-
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1115-
}
1116-
1117-
$argType = $scope->getType($exprNode->getArgs()[0]->value);
1118-
1119-
if ($argType instanceof UnionType) {
1120-
$narrowed = $this->narrowUnionByArraySize($exprNode, $argType, $constantType, $context, $scope, $rootExpr);
1121-
if ($narrowed !== null) {
1122-
return $narrowed;
1123-
}
1124-
}
1125-
1126-
if ($context->truthy() || $constantType->getValue() === 0) {
1127-
$newContext = $context;
1128-
if ($constantType->getValue() === 0) {
1129-
$newContext = $newContext->negate();
1130-
}
1131-
1132-
if ($argType->isArray()->yes()) {
1133-
if (
1134-
$context->truthy()
1135-
&& $argType->isConstantArray()->yes()
1136-
&& $constantType->isSuperTypeOf($argType->getArraySize())->no()
1137-
) {
1138-
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
1139-
}
1140-
1141-
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1142-
$constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope);
1143-
if ($context->truthy() && $constArray !== null) {
1144-
$valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr);
1145-
} else {
1146-
$valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr);
1147-
}
1148-
return $funcTypes->unionWith($valueTypes);
1149-
}
1150-
}
1151-
}
1152-
11531110
if (
11541111
!$context->null()
11551112
&& $exprNode instanceof FuncCall
@@ -2137,6 +2094,70 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21372094
}
21382095
$rightType = $scope->getType($rightExpr);
21392096

2097+
if (
2098+
!$context->null()
2099+
&& $unwrappedLeftExpr instanceof FuncCall
2100+
&& count($unwrappedLeftExpr->getArgs()) >= 1
2101+
&& $unwrappedLeftExpr->name instanceof Name
2102+
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
2103+
&& $rightType->isInteger()->yes()
2104+
) {
2105+
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
2106+
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
2107+
}
2108+
2109+
$argType = $scope->getType($unwrappedLeftExpr->getArgs()[0]->value);
2110+
$isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
2111+
if ($isZero->yes()) {
2112+
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2113+
2114+
if ($context->truthy() && !$argType->isArray()->yes()) {
2115+
$newArgType = new UnionType([
2116+
new ObjectType(Countable::class),
2117+
new ConstantArrayType([], []),
2118+
]);
2119+
} else {
2120+
$newArgType = new ConstantArrayType([], []);
2121+
}
2122+
2123+
return $funcTypes->unionWith(
2124+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $newArgType, $context, false, $scope, $rootExpr),
2125+
);
2126+
}
2127+
2128+
if ($argType instanceof UnionType) {
2129+
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr);
2130+
if ($narrowed !== null) {
2131+
return $narrowed;
2132+
}
2133+
}
2134+
2135+
if ($context->truthy()) {
2136+
if ($argType->isArray()->yes()) {
2137+
if (
2138+
$argType->isConstantArray()->yes()
2139+
&& $rightType->isSuperTypeOf($argType->getArraySize())->no()
2140+
) {
2141+
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
2142+
}
2143+
2144+
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2145+
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2146+
if ($constArray !== null) {
2147+
return $funcTypes->unionWith(
2148+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr),
2149+
);
2150+
} elseif (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2151+
return $funcTypes->unionWith(
2152+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, false, $scope, $rootExpr),
2153+
);
2154+
}
2155+
2156+
return $funcTypes;
2157+
}
2158+
}
2159+
}
2160+
21402161
if (
21412162
$context->true()
21422163
&& $unwrappedLeftExpr instanceof FuncCall

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function doFoo($arguments)
1313
return;
1414
}
1515

16-
assertType('mixed~null', $arguments);
16+
assertType('mixed~array{}|null', $arguments);
1717

1818
array_shift($arguments);
1919

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ public function doFooBar(
4444
if (count($arr) == $maybeZero) {
4545
assertType('array', $arr);
4646
} else {
47-
assertType('non-empty-array', $arr);
47+
assertType('array', $arr);
4848
}
4949
if (count($arr) === $maybeZero) {
5050
assertType('array', $arr);
5151
} else {
52-
assertType('non-empty-array', $arr);
52+
assertType('array', $arr);
5353
}
5454

5555
if (count($arr) == $negative) {
@@ -65,3 +65,24 @@ public function doFooBar(
6565
}
6666

6767
}
68+
69+
/**
70+
* @param \ArrayObject<int, mixed> $obj
71+
*/
72+
function(\ArrayObject $obj): void {
73+
if (count($obj) === 0) {
74+
assertType('ArrayObject', $obj);
75+
return;
76+
}
77+
78+
assertType('ArrayObject', $obj);
79+
};
80+
81+
function($mixed): void {
82+
if (count($mixed) === 0) {
83+
assertType('array{}|Countable', $mixed);
84+
return;
85+
}
86+
87+
assertType('mixed~array{}', $mixed);
88+
};

tests/PHPStan/Analyser/nsrt/strlen-int-range.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,17 @@ function doFoo(string $s, $zeroToThree, $twoOrThree, $twoOrMore, int $maxThree,
113113
assertType('string', $s);
114114
}
115115
}
116+
117+
/**
118+
* @param int<1, max> $oneOrMore
119+
* @param int<2, max> $twoOrMore
120+
*/
121+
function doFooBar(array $arr, int $oneOrMore, int $twoOrMore): void
122+
{
123+
if (count($arr) == $oneOrMore) {
124+
assertType('non-empty-array', $arr);
125+
}
126+
if (count($arr) === $twoOrMore) {
127+
assertType('non-empty-array', $arr);
128+
}
129+
}

0 commit comments

Comments
 (0)