Skip to content

Commit 0f91994

Browse files
committed
Improve count() narrowing of constant arrays
1 parent c586014 commit 0f91994

File tree

3 files changed

+68
-52
lines changed

3 files changed

+68
-52
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 44 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -277,22 +277,23 @@ public function specifyTypesInCondition(
277277
) {
278278
$argType = $scope->getType($expr->right->getArgs()[0]->value);
279279

280-
if ($argType instanceof UnionType) {
281-
$sizeType = null;
282-
if ($leftType instanceof ConstantIntegerType) {
283-
if ($orEqual) {
284-
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
285-
} else {
286-
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
287-
}
288-
} elseif ($leftType instanceof IntegerRangeType) {
289-
$sizeType = $leftType;
280+
$sizeType = null;
281+
if ($leftType instanceof ConstantIntegerType) {
282+
if ($orEqual) {
283+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
284+
} else {
285+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
290286
}
287+
} elseif ($leftType instanceof IntegerRangeType) {
288+
$sizeType = $leftType;
289+
}
291290

292-
$narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr);
293-
if ($narrowed !== null) {
294-
return $narrowed;
295-
}
291+
$isNormalCount = $this->isNormalCount($expr->right, $scope);
292+
$type = $isNormalCount->yes() ? $this->narrowByArraySize($argType, $sizeType, $context) : null;
293+
if ($type !== null) {
294+
$result = $result->unionWith(
295+
$this->create($expr->right->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr),
296+
);
296297
}
297298

298299
if (
@@ -1010,66 +1011,59 @@ public function specifyTypesInCondition(
10101011
return new SpecifiedTypes([], [], false, [], $rootExpr);
10111012
}
10121013

1013-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
1014+
private function narrowByArraySize(Type $type, ?Type $sizeType, TypeSpecifierContext $context): ?Type
10141015
{
10151016
if ($sizeType === null) {
10161017
return null;
10171018
}
10181019

1019-
if (count($countFuncCall->getArgs()) === 1) {
1020-
$isNormalCount = TrinaryLogic::createYes();
1021-
} else {
1022-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1023-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1024-
}
1020+
if ($type->isConstantArray()->yes()) {
1021+
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) {
1022+
if ($type instanceof UnionType) {
1023+
return $traverse($type);
1024+
}
10251025

1026-
if (
1027-
$isNormalCount->yes()
1028-
&& $argType->isConstantArray()->yes()
1029-
) {
1030-
$result = [];
1031-
foreach ($argType->getTypes() as $innerType) {
1032-
$arraySize = $innerType->getArraySize();
1026+
$arraySize = $type->getArraySize();
10331027
$isSize = $sizeType->isSuperTypeOf($arraySize);
10341028
if ($context->truthy()) {
10351029
if ($isSize->no()) {
1036-
continue;
1030+
return new NeverType();
10371031
}
10381032

1039-
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
1033+
$constArray = $this->turnListIntoConstantArray($type, $sizeType);
10401034
if ($constArray !== null) {
1041-
$innerType = $constArray;
1035+
$type = $constArray;
10421036
}
10431037
}
10441038
if ($context->falsey()) {
10451039
if (!$isSize->yes()) {
1046-
continue;
1040+
return new NeverType();
10471041
}
10481042
}
10491043

1050-
$result[] = $innerType;
1051-
}
1052-
1053-
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr);
1044+
return $type;
1045+
});
10541046
}
10551047

10561048
return null;
10571049
}
10581050

1059-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1051+
private function isNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic
10601052
{
10611053
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
10621054

10631055
if (count($countFuncCall->getArgs()) === 1) {
1064-
$isNormalCount = TrinaryLogic::createYes();
1065-
} else {
1066-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1067-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1056+
return TrinaryLogic::createYes();
10681057
}
1058+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1059+
1060+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1061+
}
10691062

1063+
private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type
1064+
{
10701065
if (
1071-
$isNormalCount->yes()
1072-
&& $type->isList()->yes()
1066+
$type->isList()->yes()
10731067
&& $sizeType instanceof ConstantIntegerType
10741068
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
10751069
) {
@@ -1083,8 +1077,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10831077
}
10841078

10851079
if (
1086-
$isNormalCount->yes()
1087-
&& $type->isList()->yes()
1080+
$type->isList()->yes()
10881081
&& $sizeType instanceof IntegerRangeType
10891082
&& $sizeType->getMin() !== null
10901083
) {
@@ -2171,11 +2164,10 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21712164
);
21722165
}
21732166

2174-
if ($argType instanceof UnionType) {
2175-
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr);
2176-
if ($narrowed !== null) {
2177-
return $narrowed;
2178-
}
2167+
$isNormalCount = $this->isNormalCount($unwrappedLeftExpr, $scope);
2168+
$type = $isNormalCount->yes() ? $this->narrowByArraySize($argType, $rightType, $context) : null;
2169+
if ($type !== null) {
2170+
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, $type, $context, false, $scope, $rootExpr);
21792171
}
21802172

21812173
if ($context->truthy()) {
@@ -2188,7 +2180,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21882180
}
21892181

21902182
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2191-
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2183+
$isNormalCount = $this->isNormalCount($unwrappedLeftExpr, $scope);
2184+
$constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null;
21922185
if ($constArray !== null) {
21932186
return $funcTypes->unionWith(
21942187
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr),

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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 23 additions & 0 deletions
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)