Skip to content

Commit 22ef97b

Browse files
committed
Improve count() narrowing of constant arrays
1 parent c586014 commit 22ef97b

File tree

3 files changed

+72
-55
lines changed

3 files changed

+72
-55
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 48 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -277,22 +277,20 @@ 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+
$specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $rootExpr);
292+
if ($specifiedTypes !== null) {
293+
$result = $result->unionWith($specifiedTypes);
296294
}
297295

298296
if (
@@ -1010,66 +1008,52 @@ public function specifyTypesInCondition(
10101008
return new SpecifiedTypes([], [], false, [], $rootExpr);
10111009
}
10121010

1013-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
1011+
private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $type, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
10141012
{
10151013
if ($sizeType === null) {
10161014
return null;
10171015
}
10181016

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-
}
1025-
10261017
if (
1027-
$isNormalCount->yes()
1028-
&& $argType->isConstantArray()->yes()
1018+
$this->isFuncCallWithNormalCount($countFuncCall, $scope)->yes()
1019+
&& $type->isConstantArray()->yes()
10291020
) {
1030-
$result = [];
1031-
foreach ($argType->getTypes() as $innerType) {
1032-
$arraySize = $innerType->getArraySize();
1021+
$resultType = TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) {
1022+
if ($type instanceof UnionType) {
1023+
return $traverse($type);
1024+
}
1025+
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-
}
1044+
return $type;
1045+
});
10521046

1053-
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, false, $scope, $rootExpr);
1047+
return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, false, $scope, $rootExpr);
10541048
}
10551049

10561050
return null;
10571051
}
10581052

1059-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1053+
private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type
10601054
{
1061-
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1062-
1063-
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());
1068-
}
1069-
10701055
if (
1071-
$isNormalCount->yes()
1072-
&& $type->isList()->yes()
1056+
$type->isList()->yes()
10731057
&& $sizeType instanceof ConstantIntegerType
10741058
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
10751059
) {
@@ -1083,8 +1067,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10831067
}
10841068

10851069
if (
1086-
$isNormalCount->yes()
1087-
&& $type->isList()->yes()
1070+
$type->isList()->yes()
10881071
&& $sizeType instanceof IntegerRangeType
10891072
&& $sizeType->getMin() !== null
10901073
) {
@@ -1121,6 +1104,18 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
11211104
return null;
11221105
}
11231106

1107+
private function isFuncCallWithNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic
1108+
{
1109+
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1110+
1111+
if (count($countFuncCall->getArgs()) === 1) {
1112+
return TrinaryLogic::createYes();
1113+
}
1114+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1115+
1116+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1117+
}
1118+
11241119
private function specifyTypesForConstantBinaryExpression(
11251120
Expr $exprNode,
11261121
Type $constantType,
@@ -2171,11 +2166,9 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21712166
);
21722167
}
21732168

2174-
if ($argType instanceof UnionType) {
2175-
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr);
2176-
if ($narrowed !== null) {
2177-
return $narrowed;
2178-
}
2169+
$specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $rootExpr);
2170+
if ($specifiedTypes !== null) {
2171+
return $specifiedTypes;
21792172
}
21802173

21812174
if ($context->truthy()) {
@@ -2188,7 +2181,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21882181
}
21892182

21902183
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, false, $scope, $rootExpr);
2191-
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2184+
$isNormalCount = $this->isFuncCallWithNormalCount($unwrappedLeftExpr, $scope);
2185+
$constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null;
21922186
if ($constArray !== null) {
21932187
return $funcTypes->unionWith(
21942188
$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)