Skip to content

Commit f618124

Browse files
authored
Fix item-type in list to constant-array conversion with count()
1 parent 07d6405 commit f618124

File tree

3 files changed

+168
-13
lines changed

3 files changed

+168
-13
lines changed

src/Analyser/TypeSpecifier.php

+43-12
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,11 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT
970970
if ($isSize->no()) {
971971
continue;
972972
}
973+
974+
$constArray = $this->turnListIntoConstantArray($countFuncCall, $innerType, $sizeType, $scope);
975+
if ($constArray !== null) {
976+
$innerType = $constArray;
977+
}
973978
}
974979
if ($context->falsey()) {
975980
if (!$isSize->yes()) {
@@ -986,6 +991,35 @@ private function narrowUnionByArraySize(FuncCall $countFuncCall, UnionType $argT
986991
return null;
987992
}
988993

994+
private function turnListIntoConstantArray(FuncCall $countFuncCall, Type $type, Type $sizeType, Scope $scope): ?Type
995+
{
996+
$argType = $scope->getType($countFuncCall->getArgs()[0]->value);
997+
998+
if (count($countFuncCall->getArgs()) === 1) {
999+
$isNormalCount = TrinaryLogic::createYes();
1000+
} else {
1001+
$mode = $scope->getType($countFuncCall->getArgs()[1]->value);
1002+
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1003+
}
1004+
1005+
if (
1006+
$isNormalCount->yes()
1007+
&& $type->isList()->yes()
1008+
&& $sizeType instanceof ConstantIntegerType
1009+
&& $sizeType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1010+
) {
1011+
// turn optional offsets non-optional
1012+
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1013+
for ($i = 0; $i < $sizeType->getValue(); $i++) {
1014+
$offsetType = new ConstantIntegerType($i);
1015+
$valueTypesBuilder->setOffsetValueType($offsetType, $type->getOffsetValueType($offsetType));
1016+
}
1017+
return $valueTypesBuilder->getArray();
1018+
}
1019+
1020+
return null;
1021+
}
1022+
9891023
private function specifyTypesForConstantBinaryExpression(
9901024
Expr $exprNode,
9911025
ConstantScalarType $constantType,
@@ -1050,21 +1084,18 @@ private function specifyTypesForConstantBinaryExpression(
10501084
}
10511085

10521086
if ($argType->isArray()->yes()) {
1053-
if (count($exprNode->getArgs()) === 1) {
1054-
$isNormalCount = TrinaryLogic::createYes();
1055-
} else {
1056-
$mode = $scope->getType($exprNode->getArgs()[1]->value);
1057-
$isNormalCount = (new ConstantIntegerType(COUNT_NORMAL))->isSuperTypeOf($mode)->or($argType->getIterableValueType()->isArray()->negate());
1087+
if (
1088+
$context->truthy()
1089+
&& $argType->isConstantArray()->yes()
1090+
&& $constantType->isSuperTypeOf($argType->getArraySize())->no()
1091+
) {
1092+
return $this->create($exprNode->getArgs()[0]->value, new NeverType(), $context, false, $scope, $rootExpr);
10581093
}
10591094

10601095
$funcTypes = $this->create($exprNode, $constantType, $context, false, $scope, $rootExpr);
1061-
if ($isNormalCount->yes() && $argType->isList()->yes() && $context->truthy() && $constantType->getValue() < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
1062-
$valueTypesBuilder = ConstantArrayTypeBuilder::createEmpty();
1063-
$itemType = $argType->getIterableValueType();
1064-
for ($i = 0; $i < $constantType->getValue(); $i++) {
1065-
$valueTypesBuilder->setOffsetValueType(new ConstantIntegerType($i), $itemType);
1066-
}
1067-
$valueTypes = $this->create($exprNode->getArgs()[0]->value, $valueTypesBuilder->getArray(), $context, false, $scope, $rootExpr);
1096+
$constArray = $this->turnListIntoConstantArray($exprNode, $argType, $constantType, $scope);
1097+
if ($context->truthy() && $constArray !== null) {
1098+
$valueTypes = $this->create($exprNode->getArgs()[0]->value, $constArray, $context, false, $scope, $rootExpr);
10681099
} else {
10691100
$valueTypes = $this->create($exprNode->getArgs()[0]->value, new NonEmptyArrayType(), $newContext, false, $scope, $rootExpr);
10701101
}

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

+124
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,127 @@ function countCountable(CountableFoo $x, int $mode)
216216
}
217217
assertType('ListCount\CountableFoo', $x);
218218
}
219+
220+
class CountWithOptionalKeys
221+
{
222+
/**
223+
* @param array{0: mixed, 1?: string|null} $row
224+
*/
225+
protected function testOptionalKeys($row): void
226+
{
227+
if (count($row) === 0) {
228+
assertType('*NEVER*', $row);
229+
} else {
230+
assertType('array{0: mixed, 1?: string|null}', $row);
231+
}
232+
233+
if (count($row) === 1) {
234+
assertType('array{mixed}', $row);
235+
} else {
236+
assertType('array{0: mixed, 1?: string|null}', $row);
237+
}
238+
239+
if (count($row) === 2) {
240+
assertType('array{mixed, string|null}', $row);
241+
} else {
242+
assertType('array{0: mixed, 1?: string|null}', $row);
243+
}
244+
245+
if (count($row) === 3) {
246+
assertType('*NEVER*', $row);
247+
} else {
248+
assertType('array{0: mixed, 1?: string|null}', $row);
249+
}
250+
}
251+
252+
/**
253+
* @param array{mixed}|array{0: mixed, 1?: string|null} $row
254+
*/
255+
protected function testOptionalKeysInUnion($row): void
256+
{
257+
if (count($row) === 0) {
258+
assertType('*NEVER*', $row);
259+
} else {
260+
assertType('array{0: mixed, 1?: string|null}', $row);
261+
}
262+
263+
if (count($row) === 1) {
264+
assertType('array{mixed}', $row);
265+
} else {
266+
assertType('array{0: mixed, 1?: string|null}', $row);
267+
}
268+
269+
if (count($row) === 2) {
270+
assertType('array{mixed, string|null}', $row);
271+
} else {
272+
assertType('array{0: mixed, 1?: string|null}', $row);
273+
}
274+
275+
if (count($row) === 3) {
276+
assertType('*NEVER*', $row);
277+
} else {
278+
assertType('array{0: mixed, 1?: string|null}', $row);
279+
}
280+
}
281+
282+
/**
283+
* @param array{string}|array{0: int, 1?: string|null} $row
284+
*/
285+
protected function testOptionalKeysInListsOfTaggedUnion($row): void
286+
{
287+
if (count($row) === 0) {
288+
assertType('*NEVER*', $row);
289+
} else {
290+
assertType('array{0: int, 1?: string|null}|array{string}', $row);
291+
}
292+
293+
if (count($row) === 1) {
294+
assertType('array{0: int, 1?: string|null}|array{string}', $row);
295+
} else {
296+
assertType('array{0: int, 1?: string|null}', $row);
297+
}
298+
299+
if (count($row) === 2) {
300+
assertType('array{int, string|null}', $row);
301+
} else {
302+
assertType('array{0: int, 1?: string|null}|array{string}', $row);
303+
}
304+
305+
if (count($row) === 3) {
306+
assertType('*NEVER*', $row);
307+
} else {
308+
assertType('array{0: int, 1?: string|null}|array{string}', $row);
309+
}
310+
}
311+
312+
/**
313+
* @param array{string}|array{0: int, 3?: string|null} $row
314+
*/
315+
protected function testOptionalKeysInUnionArray($row): void
316+
{
317+
if (count($row) === 0) {
318+
assertType('*NEVER*', $row);
319+
} else {
320+
assertType('array{0: int, 3?: string|null}|array{string}', $row);
321+
}
322+
323+
if (count($row) === 1) {
324+
assertType('array{0: int, 3?: string|null}|array{string}', $row);
325+
} else {
326+
assertType('array{0: int, 3?: string|null}', $row);
327+
}
328+
329+
if (count($row) === 2) {
330+
assertType('array{0: int, 3?: string|null}', $row);
331+
} else {
332+
assertType('array{0: int, 3?: string|null}|array{string}', $row);
333+
}
334+
335+
if (count($row) === 3) {
336+
assertType('*NEVER*', $row);
337+
} else {
338+
assertType('array{0: int, 3?: string|null}|array{string}', $row);
339+
}
340+
}
341+
342+
}

tests/PHPStan/Analyser/nsrt/preg_match_shapes.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ function bug11277a(string $value): void
330330
if (preg_match('/^\[(.+,?)*\]$/', $value, $matches)) {
331331
assertType('array{0: string, 1?: non-empty-string}', $matches);
332332
if (count($matches) === 2) {
333-
assertType('array{string, string}', $matches); // could be array{string, non-empty-string}
333+
assertType('array{string, non-empty-string}', $matches);
334334
}
335335
}
336336
}

0 commit comments

Comments
 (0)