Skip to content

Commit e411d50

Browse files
committed
Improve count() narrowing of constant arrays
1 parent 7d4dcb5 commit e411d50

File tree

3 files changed

+72
-55
lines changed

3 files changed

+72
-55
lines changed

src/Analyser/TypeSpecifier.php

+48-54
Original file line numberDiff line numberDiff line change
@@ -272,22 +272,20 @@ public function specifyTypesInCondition(
272272
) {
273273
$argType = $scope->getType($expr->right->getArgs()[0]->value);
274274

275-
if ($argType instanceof UnionType) {
276-
$sizeType = null;
277-
if ($leftType instanceof ConstantIntegerType) {
278-
if ($orEqual) {
279-
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
280-
} else {
281-
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
282-
}
283-
} elseif ($leftType instanceof IntegerRangeType) {
284-
$sizeType = $leftType;
275+
$sizeType = null;
276+
if ($leftType instanceof ConstantIntegerType) {
277+
if ($orEqual) {
278+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
279+
} else {
280+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
285281
}
282+
} elseif ($leftType instanceof IntegerRangeType) {
283+
$sizeType = $leftType;
284+
}
286285

287-
$narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $expr);
288-
if ($narrowed !== null) {
289-
return $narrowed;
290-
}
286+
$specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr);
287+
if ($specifiedTypes !== null) {
288+
$result = $result->unionWith($specifiedTypes);
291289
}
292290

293291
if (
@@ -1046,66 +1044,52 @@ public function specifyTypesInCondition(
10461044
return (new SpecifiedTypes([], []))->setRootExpr($expr);
10471045
}
10481046

1049-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
1047+
private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $type, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
10501048
{
10511049
if ($sizeType === null) {
10521050
return null;
10531051
}
10541052

1055-
if (count($countFuncCall->getArgs()) === 1) {
1056-
$isNormalCount = TrinaryLogic::createYes();
1057-
} else {
1058-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1059-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1060-
}
1061-
10621053
if (
1063-
$isNormalCount->yes()
1064-
&& $argType->isConstantArray()->yes()
1054+
$this->isFuncCallWithNormalCount($countFuncCall, $scope)->yes()
1055+
&& $type->isConstantArray()->yes()
10651056
) {
1066-
$result = [];
1067-
foreach ($argType->getTypes() as $innerType) {
1068-
$arraySize = $innerType->getArraySize();
1057+
$resultType = TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) {
1058+
if ($type instanceof UnionType) {
1059+
return $traverse($type);
1060+
}
1061+
1062+
$arraySize = $type->getArraySize();
10691063
$isSize = $sizeType->isSuperTypeOf($arraySize);
10701064
if ($context->truthy()) {
10711065
if ($isSize->no()) {
1072-
continue;
1066+
return new NeverType();
10731067
}
10741068

1075-
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
1069+
$constArray = $this->turnListIntoConstantArray($type, $sizeType);
10761070
if ($constArray !== null) {
1077-
$innerType = $constArray;
1071+
$type = $constArray;
10781072
}
10791073
}
10801074
if ($context->falsey()) {
10811075
if (!$isSize->yes()) {
1082-
continue;
1076+
return new NeverType();
10831077
}
10841078
}
10851079

1086-
$result[] = $innerType;
1087-
}
1080+
return $type;
1081+
});
10881082

1089-
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, $scope)->setRootExpr($rootExpr);
1083+
return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr);
10901084
}
10911085

10921086
return null;
10931087
}
10941088

1095-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1089+
private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type
10961090
{
1097-
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1098-
1099-
if (count($countFuncCall->getArgs()) === 1) {
1100-
$isNormalCount = TrinaryLogic::createYes();
1101-
} else {
1102-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1103-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1104-
}
1105-
11061091
if (
1107-
$isNormalCount->yes()
1108-
&& $type->isList()->yes()
1092+
$type->isList()->yes()
11091093
&& $sizeType instanceof ConstantIntegerType
11101094
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
11111095
) {
@@ -1119,8 +1103,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
11191103
}
11201104

11211105
if (
1122-
$isNormalCount->yes()
1123-
&& $type->isList()->yes()
1106+
$type->isList()->yes()
11241107
&& $sizeType instanceof IntegerRangeType
11251108
&& $sizeType->getMin() !== null
11261109
) {
@@ -1157,6 +1140,18 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
11571140
return null;
11581141
}
11591142

1143+
private function isFuncCallWithNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic
1144+
{
1145+
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1146+
1147+
if (count($countFuncCall->getArgs()) === 1) {
1148+
return TrinaryLogic::createYes();
1149+
}
1150+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1151+
1152+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1153+
}
1154+
11601155
private function specifyTypesForConstantBinaryExpression(
11611156
Expr $exprNode,
11621157
Type $constantType,
@@ -2186,11 +2181,9 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21862181
);
21872182
}
21882183

2189-
if ($argType instanceof UnionType) {
2190-
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2191-
if ($narrowed !== null) {
2192-
return $narrowed;
2193-
}
2184+
$specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2185+
if ($specifiedTypes !== null) {
2186+
return $specifiedTypes;
21942187
}
21952188

21962189
if ($context->truthy()) {
@@ -2203,7 +2196,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
22032196
}
22042197

22052198
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2206-
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2199+
$isNormalCount = $this->isFuncCallWithNormalCount($unwrappedLeftExpr, $scope);
2200+
$constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null;
22072201
if ($constArray !== null) {
22082202
return $funcTypes->unionWith(
22092203
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, $scope)->setRootExpr($expr),

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function(array $array, int $count): void {
4040
if (isset($array['d'])) $a[] = $array['d'];
4141
if (isset($array['e'])) $a[] = $array['e'];
4242
if (count($a) > $count) {
43-
assertType('int<1, 5>', count($a));
43+
assertType('int<2, 5>', count($a));
4444
assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
4545
} else {
4646
assertType('0', count($a));

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

+23
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ public function doFooBar(
6464
}
6565
}
6666

67+
/** @param array{0: string, 1?: string} $arr */
68+
public function doBar(array $arr): void
69+
{
70+
if (count($arr) <= 1) {
71+
assertType('1', count($arr));
72+
return;
73+
}
74+
75+
assertType('2', count($arr));
76+
assertType('array{string, string}', $arr);
77+
}
78+
79+
/** @param array{0: string, 1?: string} $arr */
80+
public function doBaz(array $arr): void
81+
{
82+
if (count($arr) > 1) {
83+
assertType('2', count($arr));
84+
assertType('array{string, string}', $arr);
85+
}
86+
87+
assertType('1|2', count($arr));
88+
}
89+
6790
}
6891

6992
/**

0 commit comments

Comments
 (0)