Skip to content

Commit 891412d

Browse files
committed
Improve count() narrowing of constant arrays
1 parent c586014 commit 891412d

File tree

3 files changed

+67
-50
lines changed

3 files changed

+67
-50
lines changed

src/Analyser/TypeSpecifier.php

+43-49
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,64 @@ 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->isNormalCount($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 isNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic
10601054
{
10611055
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
10621056

10631057
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());
1058+
return TrinaryLogic::createYes();
10681059
}
1060+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1061+
1062+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1063+
}
10691064

1065+
private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type
1066+
{
10701067
if (
1071-
$isNormalCount->yes()
1072-
&& $type->isList()->yes()
1068+
$type->isList()->yes()
10731069
&& $sizeType instanceof ConstantIntegerType
10741070
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
10751071
) {
@@ -1083,8 +1079,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10831079
}
10841080

10851081
if (
1086-
$isNormalCount->yes()
1087-
&& $type->isList()->yes()
1082+
$type->isList()->yes()
10881083
&& $sizeType instanceof IntegerRangeType
10891084
&& $sizeType->getMin() !== null
10901085
) {
@@ -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->isNormalCount($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

+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)