Skip to content

Commit b71049f

Browse files
committed
Improve count() narrowing of constant arrays
1 parent ae3a484 commit b71049f

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
@@ -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 (
@@ -972,66 +970,52 @@ public function specifyTypesInCondition(
972970
return (new SpecifiedTypes([], []))->setRootExpr($expr);
973971
}
974972

975-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
973+
private function specifyTypesForCountFuncCall(FuncCall $countFuncCall, Type $type, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
976974
{
977975
if ($sizeType === null) {
978976
return null;
979977
}
980978

981-
if (count($countFuncCall->getArgs()) === 1) {
982-
$isNormalCount = TrinaryLogic::createYes();
983-
} else {
984-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
985-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
986-
}
987-
988979
if (
989-
$isNormalCount->yes()
990-
&& $argType->isConstantArray()->yes()
980+
$this->isFuncCallWithNormalCount($countFuncCall, $scope)->yes()
981+
&& $type->isConstantArray()->yes()
991982
) {
992-
$result = [];
993-
foreach ($argType->getTypes() as $innerType) {
994-
$arraySize = $innerType->getArraySize();
983+
$resultType = TypeTraverser::map($type, function (Type $type, callable $traverse) use ($sizeType, $context) {
984+
if ($type instanceof UnionType) {
985+
return $traverse($type);
986+
}
987+
988+
$arraySize = $type->getArraySize();
995989
$isSize = $sizeType->isSuperTypeOf($arraySize);
996990
if ($context->truthy()) {
997991
if ($isSize->no()) {
998-
continue;
992+
return new NeverType();
999993
}
1000994

1001-
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
995+
$constArray = $this->turnListIntoConstantArray($type, $sizeType);
1002996
if ($constArray !== null) {
1003-
$innerType = $constArray;
997+
$type = $constArray;
1004998
}
1005999
}
10061000
if ($context->falsey()) {
10071001
if (!$isSize->yes()) {
1008-
continue;
1002+
return new NeverType();
10091003
}
10101004
}
10111005

1012-
$result[] = $innerType;
1013-
}
1006+
return $type;
1007+
});
10141008

1015-
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, $scope)->setRootExpr($rootExpr);
1009+
return $this->create($countFuncCall->getArgs()[0]->value, $resultType, $context, $scope)->setRootExpr($rootExpr);
10161010
}
10171011

10181012
return null;
10191013
}
10201014

1021-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1015+
private function turnListIntoConstantArray(Type $type, Type $sizeType): ?Type
10221016
{
1023-
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1024-
1025-
if (count($countFuncCall->getArgs()) === 1) {
1026-
$isNormalCount = TrinaryLogic::createYes();
1027-
} else {
1028-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1029-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1030-
}
1031-
10321017
if (
1033-
$isNormalCount->yes()
1034-
&& $type->isList()->yes()
1018+
$type->isList()->yes()
10351019
&& $sizeType instanceof ConstantIntegerType
10361020
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
10371021
) {
@@ -1045,8 +1029,7 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10451029
}
10461030

10471031
if (
1048-
$isNormalCount->yes()
1049-
&& $type->isList()->yes()
1032+
$type->isList()->yes()
10501033
&& $sizeType instanceof IntegerRangeType
10511034
&& $sizeType->getMin() !== null
10521035
) {
@@ -1083,6 +1066,18 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10831066
return null;
10841067
}
10851068

1069+
private function isFuncCallWithNormalCount(FuncCall $countFuncCall, Scope $scope): TrinaryLogic
1070+
{
1071+
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1072+
1073+
if (count($countFuncCall->getArgs()) === 1) {
1074+
return TrinaryLogic::createYes();
1075+
}
1076+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1077+
1078+
return (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1079+
}
1080+
10861081
private function specifyTypesForConstantBinaryExpression(
10871082
Expr $exprNode,
10881083
Type $constantType,
@@ -2112,11 +2107,9 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21122107
);
21132108
}
21142109

2115-
if ($argType instanceof UnionType) {
2116-
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2117-
if ($narrowed !== null) {
2118-
return $narrowed;
2119-
}
2110+
$specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2111+
if ($specifiedTypes !== null) {
2112+
return $specifiedTypes;
21202113
}
21212114

21222115
if ($context->truthy()) {
@@ -2129,7 +2122,8 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21292122
}
21302123

21312124
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2132-
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2125+
$isNormalCount = $this->isFuncCallWithNormalCount($unwrappedLeftExpr, $scope);
2126+
$constArray = $isNormalCount->yes() ? $this->turnListIntoConstantArray($argType, $rightType) : null;
21332127
if ($constArray !== null) {
21342128
return $funcTypes->unionWith(
21352129
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, $scope)->setRootExpr($expr),

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)