Skip to content

Commit 60b29fa

Browse files
authored
Improve count() narrowing of constant arrays
1 parent b0e044e commit 60b29fa

File tree

6 files changed

+150
-132
lines changed

6 files changed

+150
-132
lines changed

src/Analyser/TypeSpecifier.php

+90-127
Original file line numberDiff line numberDiff line change
@@ -272,22 +272,21 @@ 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+
if ($leftType instanceof ConstantIntegerType) {
276+
if ($orEqual) {
277+
$sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue());
278+
} else {
279+
$sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue());
285280
}
281+
} elseif ($leftType instanceof IntegerRangeType) {
282+
$sizeType = $leftType->shift($offset);
283+
} else {
284+
$sizeType = $leftType;
285+
}
286286

287-
$narrowed = $this->narrowUnionByArraySize($expr->right, $argType, $sizeType, $context, $scope, $expr);
288-
if ($narrowed !== null) {
289-
return $narrowed;
290-
}
287+
$specifiedTypes = $this->specifyTypesForCountFuncCall($expr->right, $argType, $sizeType, $context, $scope, $expr);
288+
if ($specifiedTypes !== null) {
289+
$result = $result->unionWith($specifiedTypes);
291290
}
292291

293292
if (
@@ -1046,115 +1045,95 @@ public function specifyTypesInCondition(
10461045
return (new SpecifiedTypes([], []))->setRootExpr($expr);
10471046
}
10481047

1049-
private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argType, ?Type $sizeType, TypeSpecifierContext $context, Scope $scope, ?Expr $rootExpr): ?SpecifiedTypes
1048+
private function specifyTypesForCountFuncCall(
1049+
FuncCall $countFuncCall,
1050+
Type $type,
1051+
Type $sizeType,
1052+
TypeSpecifierContext $context,
1053+
Scope $scope,
1054+
Expr $rootExpr,
1055+
): ?SpecifiedTypes
10501056
{
1051-
if ($sizeType === null) {
1052-
return null;
1053-
}
1054-
10551057
if (count($countFuncCall->getArgs()) === 1) {
10561058
$isNormalCount = TrinaryLogic::createYes();
10571059
} else {
10581060
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1059-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1061+
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($type->getIterableValueType()->isArray()->negate());
10601062
}
10611063

1064+
$isList = $type->isList();
10621065
if (
1063-
$isNormalCount->yes()
1064-
&& $argType->isConstantArray()->yes()
1066+
!$isNormalCount->yes()
1067+
|| (!$type->isConstantArray()->yes() && !$isList->yes())
1068+
|| $type->isIterableAtLeastOnce()->no() // array{} cannot be used for further narrowing
10651069
) {
1066-
$result = [];
1067-
foreach ($argType->getTypes() as $innerType) {
1068-
$arraySize = $innerType->getArraySize();
1069-
$isSize = $sizeType->isSuperTypeOf($arraySize);
1070-
if ($context->truthy()) {
1071-
if ($isSize->no()) {
1072-
continue;
1073-
}
1074-
1075-
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
1076-
if ($constArray !== null) {
1077-
$innerType = $constArray;
1078-
}
1079-
}
1080-
if ($context->falsey()) {
1081-
if (!$isSize->yes()) {
1082-
continue;
1083-
}
1084-
}
1085-
1086-
$result[] = $innerType;
1087-
}
1088-
1089-
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$result), $context, $scope)->setRootExpr($rootExpr);
1070+
return null;
10901071
}
10911072

1092-
return null;
1093-
}
1094-
1095-
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
1096-
{
1097-
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
1098-
1099-
if (count($countFuncCall->getArgs()) === 1) {
1100-
$isNormalCount = TrinaryLogic::createYes();
1101-
} else {
1102-
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1103-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->result->or($argType->getIterableValueType()->isArray()->negate());
1104-
}
1073+
$resultTypes = [];
1074+
foreach ($type->getArrays() as $arrayType) {
1075+
$isSizeSuperTypeOfArraySize = $sizeType->isSuperTypeOf($arrayType->getArraySize());
1076+
if ($isSizeSuperTypeOfArraySize->no()) {
1077+
continue;
1078+
}
11051079

1106-
if (
1107-
$isNormalCount->yes()
1108-
&& $type->isList()->yes()
1109-
&& $sizeType instanceof ConstantIntegerType
1110-
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1111-
) {
1112-
// turn optional offsets non-optional
1113-
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1114-
for ($i = 0; $i < $sizeType->getValue(); $i++) {
1115-
$offsetType = new ConstantIntegerType($i);
1116-
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType));
1080+
if ($context->falsey() && $isSizeSuperTypeOfArraySize->maybe()) {
1081+
continue;
11171082
}
1118-
return $valueTypesBuilder->getArray();
1119-
}
11201083

1121-
if (
1122-
$isNormalCount->yes()
1123-
&& $type->isList()->yes()
1124-
&& $sizeType instanceof IntegerRangeType
1125-
&& $sizeType->getMin() !== null
1126-
) {
1127-
// turn optional offsets non-optional
1128-
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1129-
for ($i = 0; $i < $sizeType->getMin(); $i++) {
1130-
$offsetType = new ConstantIntegerType($i);
1131-
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType));
1132-
}
1133-
if ($sizeType->getMax() !== null) {
1134-
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
1084+
if (
1085+
$isList->yes()
1086+
&& $sizeType instanceof ConstantIntegerType
1087+
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1088+
) {
1089+
// turn optional offsets non-optional
1090+
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1091+
for ($i = 0; $i < $sizeType->getValue(); $i++) {
11351092
$offsetType = new ConstantIntegerType($i);
1136-
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), true);
1093+
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
11371094
}
1138-
} elseif ($type->isConstantArray()->yes()) {
1139-
for ($i = $sizeType->getMin();; $i++) {
1095+
$resultTypes[] = $valueTypesBuilder->getArray();
1096+
continue;
1097+
}
1098+
1099+
if (
1100+
$isList->yes()
1101+
&& $sizeType instanceof IntegerRangeType
1102+
&& $sizeType->getMin() !== null
1103+
) {
1104+
// turn optional offsets non-optional
1105+
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1106+
for ($i = 0; $i < $sizeType->getMin(); $i++) {
11401107
$offsetType = new ConstantIntegerType($i);
1141-
$hasOffset = $type->hasOffsetValueType($offsetType);
1142-
if ($hasOffset->no()) {
1143-
break;
1108+
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType));
1109+
}
1110+
if ($sizeType->getMax() !== null) {
1111+
for ($i = $sizeType->getMin(); $i < $sizeType->getMax(); $i++) {
1112+
$offsetType = new ConstantIntegerType($i);
1113+
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), true);
11441114
}
1145-
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType), !$hasOffset->yes());
1115+
} elseif ($arrayType->isConstantArray()->yes()) {
1116+
for ($i = $sizeType->getMin();; $i++) {
1117+
$offsetType = new ConstantIntegerType($i);
1118+
$hasOffset = $arrayType->hasOffsetValueType($offsetType);
1119+
if ($hasOffset->no()) {
1120+
break;
1121+
}
1122+
$valueTypesBuilder->setOffsetValueType($offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes());
1123+
}
1124+
} else {
1125+
$resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
1126+
continue;
11461127
}
1147-
} else {
1148-
return null;
1149-
}
11501128

1151-
$arrayType = $valueTypesBuilder->getArray();
1152-
if ($arrayType->isIterableAtLeastOnce()->yes()) {
1153-
return $arrayType;
1129+
$resultTypes[] = $valueTypesBuilder->getArray();
1130+
continue;
11541131
}
1132+
1133+
$resultTypes[] = $arrayType;
11551134
}
11561135

1157-
return null;
1136+
return $this->create($countFuncCall->getArgs()[0]->value, TypeCombinator::union(...$resultTypes), $context, $scope)->setRootExpr($rootExpr);
11581137
}
11591138

11601139
private function specifyTypesForConstantBinaryExpression(
@@ -2186,36 +2165,20 @@ public function resolveIdentical(Expr\BinaryOp\Identical $expr, Scope $scope, Ty
21862165
);
21872166
}
21882167

2189-
if ($argType instanceof UnionType) {
2190-
$narrowed = $this->narrowUnionByArraySize($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2191-
if ($narrowed !== null) {
2192-
return $narrowed;
2193-
}
2168+
$specifiedTypes = $this->specifyTypesForCountFuncCall($unwrappedLeftExpr, $argType, $rightType, $context, $scope, $expr);
2169+
if ($specifiedTypes !== null) {
2170+
return $specifiedTypes;
21942171
}
21952172

2196-
if ($context->truthy()) {
2197-
if ($argType->isArray()->yes()) {
2198-
if (
2199-
$argType->isConstantArray()->yes()
2200-
&& $rightType->isSuperTypeOf($argType->getArraySize())->no()
2201-
) {
2202-
return $this->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), $context, $scope)->setRootExpr($expr);
2203-
}
2204-
2205-
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2206-
$constArray = $this->turnListIntoConstantArray($unwrappedLeftExpr, $argType, $rightType, $scope);
2207-
if ($constArray !== null) {
2208-
return $funcTypes->unionWith(
2209-
$this->create($unwrappedLeftExpr->getArgs()[0]->value, $constArray, $context, $scope)->setRootExpr($expr),
2210-
);
2211-
} elseif (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2212-
return $funcTypes->unionWith(
2213-
$this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
2214-
);
2215-
}
2216-
2217-
return $funcTypes;
2173+
if ($context->truthy() && $argType->isArray()->yes()) {
2174+
$funcTypes = $this->create($unwrappedLeftExpr, $rightType, $context, $scope)->setRootExpr($expr);
2175+
if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
2176+
return $funcTypes->unionWith(
2177+
$this->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), $context, $scope)->setRootExpr($expr),
2178+
);
22182179
}
2180+
2181+
return $funcTypes;
22192182
}
22202183
}
22212184

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -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<1, 5>', count($a));
44-
assertType('array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
43+
assertType('int<2, 5>', count($a));
44+
assertType('list{0: mixed~null, 1: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a);
4545
} else {
46-
assertType('0', count($a));
47-
assertType('array{}', $a);
46+
assertType('int<0, 5>', count($a)); // Could be int<0, 1>
47+
assertType('array{}|array{0: mixed~null, 1?: mixed~null, 2?: mixed~null, 3?: mixed~null, 4?: mixed~null}', $a); // Could be array{}|array{0: mixed~null}
4848
}
4949
};

tests/PHPStan/Analyser/nsrt/bug11480.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public function intUnionCount(): void
8484
if (count($x) >= $count) {
8585
assertType("array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
8686
} else {
87-
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
87+
assertType("array{}", $x);
8888
}
8989
assertType("array{}|array{'xy'}|array{0: 'ab', 1?: 'xy'}", $x);
9090
}

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
/**

tests/PHPStan/Rules/Functions/CallToFunctionParametersRuleTest.php

+5
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,11 @@ public function testBug3608(): void
545545
$this->analyse([__DIR__ . '/data/bug-3608.php'], []);
546546
}
547547

548+
public function testBug3631(): void
549+
{
550+
$this->analyse([__DIR__ . '/data/bug-3631.php'], []);
551+
}
552+
548553
public function testBug3920(): void
549554
{
550555
$this->analyse([__DIR__ . '/data/bug-3920.php'], []);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3631;
4+
5+
/**
6+
* @return array<string>
7+
*/
8+
function someFunc(bool $flag): array
9+
{
10+
$ids = [
11+
['fa', 'foo', 'baz']
12+
];
13+
14+
if ($flag) {
15+
$ids[] = ['foo', 'bar', 'baz'];
16+
17+
}
18+
19+
if (count($ids) > 1) {
20+
return array_intersect(...$ids);
21+
}
22+
23+
return $ids[0];
24+
}
25+
26+
var_dump(someFunc(true));
27+
var_dump(someFunc(false));

0 commit comments

Comments
 (0)