Skip to content

Commit 781a5da

Browse files
committed
Narrow arrays in union based on count() with IntegerRangeType
1 parent bbd64a9 commit 781a5da

File tree

4 files changed

+105
-17
lines changed

4 files changed

+105
-17
lines changed

src/Analyser/TypeSpecifier.php

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,16 @@ public function specifyTypesInCondition(
246246
) {
247247
$argType = $scope->getType($expr->right->getArgs()[0]->value);
248248

249-
if ($argType instanceof UnionType && $leftType instanceof ConstantIntegerType) {
250-
if ($orEqual) {
251-
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
252-
} else {
253-
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
249+
if ($argType instanceof UnionType) {
250+
$sizeType = null;
251+
if ($leftType instanceof ConstantIntegerType) {
252+
if ($orEqual) {
253+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
254+
} else {
255+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
256+
}
257+
} elseif ($leftType instanceof IntegerRangeType) {
258+
$sizeType = $leftType;
254259
}
255260

256261
$narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $rootExpr);
@@ -949,8 +954,12 @@ public function specifyTypesInCondition(
949954
return new SpecifiedTypes([], [], false, [], $rootExpr);
950955
}
951956

952-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
957+
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
953958
{
959+
if ($sizeType === null) {
960+
return null;
961+
}
962+
954963
if (count($countFuncCall->getArgs()) === 1) {
955964
$isNormalCount = TrinaryLogic::createYes();
956965
} else {
@@ -971,7 +980,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT
971980
continue;
972981
}
973982

974-
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
983+
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $context, $scope);
975984
if ($constArray !== null) {
976985
$innerType = $constArray;
977986
}
@@ -991,7 +1000,7 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT
9911000
return null;
9921001
}
9931002

994-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1003+
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, TypeSpecifierContext $context, Scope $scope): ?Type
9951004
{
9961005
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
9971006

@@ -1017,6 +1026,38 @@ private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type,
10171026
return $valueTypesBuilder->getArray();
10181027
}
10191028

1029+
if (
1030+
$context->truthy()
1031+
&& $isNormalCount->yes()
1032+
&& $type->isList()->yes()
1033+
&& $sizeType instanceof IntegerRangeType
1034+
&& $sizeType->getMin() !== null
1035+
) {
1036+
// turn optional offsets non-optional
1037+
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1038+
for ($i = 0; $i < $sizeType->getMin(); $i++) {
1039+
$offsetType = new ConstantIntegerType($i);
1040+
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType));
1041+
}
1042+
if ($sizeType->getMax() !== null) {
1043+
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
1044+
$offsetType = new ConstantIntegerType($i);
1045+
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true);
1046+
}
1047+
} else {
1048+
for ($i = $sizeType->getMin();; $i++) {
1049+
$offsetType = new ConstantIntegerType($i);
1050+
$hasOffset = $type->hasOffsetValueType($offsetType);
1051+
if ($hasOffset->no()) {
1052+
break;
1053+
}
1054+
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes());
1055+
}
1056+
1057+
}
1058+
return $valueTypesBuilder->getArray();
1059+
}
1060+
10201061
return null;
10211062
}
10221063

@@ -1093,7 +1134,7 @@ private function specifyTypesForConstantBinaryExpression(
10931134
}
10941135

10951136
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1096-
$constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope);
1137+
$constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $context, $scope);
10971138
if ($context->truthy() && $constArray !== null) {
10981139
$valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr);
10991140
} else {

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ function(array $array, int $count): void {
2121
assertType('int<1, 5>', count($a));
2222
assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
2323
} else {
24-
assertType('int<0, 5>', count($a));
25-
assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
24+
assertType('0', count($a));
25+
assertType('array{}', $a);
2626
}
2727
};
2828

@@ -40,10 +40,10 @@ 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<2, 5>', count($a));
43+
assertType('int<1, 5>', count($a));
4444
assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
4545
} else {
46-
assertType('int<0, 5>', count($a));
47-
assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
46+
assertType('0', count($a));
47+
assertType('array{}', $a);
4848
}
4949
};

tests/PHPStan/Analyser/nsrt/bug11480.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public function arrayGreatherThan(): void
2424
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
2525

2626
if (count($x) > 1) {
27-
assertType("array{0: 'ab', 1?: 'xy'}", $x);
27+
assertType("array{'ab', 'xy'}", $x);
2828
} else {
2929
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
3030
}
@@ -58,7 +58,7 @@ public function arraySmallerThan(): void
5858
if (count($x) <= 1) {
5959
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
6060
} else {
61-
assertType("array{0: 'ab', 1?: 'xy'}", $x);
61+
assertType("array{'ab', 'xy'}", $x);
6262
}
6363
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
6464
}
@@ -106,7 +106,7 @@ public function intRangeCount($count): void
106106
if (count($x) >= $count) {
107107
assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
108108
} else {
109-
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
109+
assertType("array{}", $x);
110110
}
111111
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
112112
}

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,4 +339,51 @@ protected function testOptionalKeysInUnionArray($row): void
339339
}
340340
}
341341

342+
/**
343+
* @param array{string}|list{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row
344+
* @param int<2, 3> $twoOrThree
345+
* @param int<2, max> $twoOrMore
346+
* @param int<min, 3> $maxThree
347+
* @param int<10, 11> $tenOrEleven
348+
*/
349+
protected function testOptionalKeysInUnionListWithIntRange($row, $twoOrThree, $twoOrMore, int $maxThree, $tenOrEleven): void
350+
{
351+
if (count($row) >= $twoOrThree) {
352+
assertType('array{0: int, 1: string|null, 2?: int|null}', $row);
353+
} else {
354+
assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row);
355+
}
356+
357+
if (count($row) >= $tenOrEleven) {
358+
assertType('*NEVER*', $row);
359+
} else {
360+
assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row);
361+
}
362+
363+
if (count($row) >= $twoOrMore) {
364+
assertType('array{0: int, 1: string|null, 2?: int|null, 3?: float|null}&list', $row);
365+
} else {
366+
assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row);
367+
}
368+
369+
if (count($row) >= $maxThree) {
370+
assertType('(array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list)|array{string}', $row);
371+
} else {
372+
assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}&list', $row);
373+
}
374+
}
375+
376+
/**
377+
* @param array{string}|array{0: int, 1?: string|null, 2?: int|null, 3?: float|null} $row
378+
* @param int<2, 3> $twoOrThree
379+
*/
380+
protected function testOptionalKeysInUnionArrayWithIntRange($row, $twoOrThree): void
381+
{
382+
// doesn't narrow because no list
383+
if (count($row) >= $twoOrThree) {
384+
assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}', $row);
385+
} else {
386+
assertType('array{0: int, 1?: string|null, 2?: int|null, 3?: float|null}|array{string}', $row);
387+
}
388+
}
342389
}

0 commit comments

Comments
 (0)