diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 6698a8a800..48d9494406 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -38,6 +38,7 @@ use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Accessory\HasOffsetType; +use PHPStan\Type\Accessory\HasOffsetValueType; use PHPStan\Type\Accessory\HasPropertyType; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; @@ -92,6 +93,8 @@ final class TypeSpecifier { + private const MAX_ACCESSORIES_LIMIT = 8; + /** @var MethodTypeSpecifyingExtension[][]|null */ private ?array $methodTypeSpecifyingExtensionsByClass = null; @@ -1197,7 +1200,27 @@ private function specifyTypesForCountFuncCall( $builderData[] = [$offsetType, $arrayType->getOffsetValueType($offsetType), !$hasOffset->yes()]; } } else { - $resultTypes[] = TypeCombinator::intersect($arrayType, new NonEmptyArrayType()); + $intersection = []; + $intersection[] = $arrayType; + $intersection[] = new NonEmptyArrayType(); + + $zero = new ConstantIntegerType(0); + $i = 0; + foreach ($builderData as [$offsetType, $valueType]) { + // non-empty-list already implies the offset 0 + if ($zero->isSuperTypeOf($offsetType)->yes()) { + continue; + } + + if ($i > self::MAX_ACCESSORIES_LIMIT) { + break; + } + + $intersection[] = new HasOffsetValueType($offsetType, $valueType); + $i++; + } + + $resultTypes[] = TypeCombinator::intersect(...$intersection); continue; } diff --git a/tests/PHPStan/Analyser/nsrt/bug-11642.php b/tests/PHPStan/Analyser/nsrt/bug-11642.php index 520cf772bf..7c72706fe5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-11642.php +++ b/tests/PHPStan/Analyser/nsrt/bug-11642.php @@ -34,6 +34,7 @@ function doFoo() { if (count($entries) !== count($payload->ids)) { exit(); } + assertType('int<1, max>', count($entries)); assertType('non-empty-list', $entries); if (count($entries) > 3) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-13747.php b/tests/PHPStan/Analyser/nsrt/bug-13747.php new file mode 100644 index 0000000000..98ca6f6a91 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-13747.php @@ -0,0 +1,93 @@ + $list */ + public function count($list): void + { + if (count($list) === 0) { + return; + } + + if (count($list) > 2) { + assertType('non-empty-list&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list); + assertType('int<3, max>', count($list)); + } else { + assertType('non-empty-list', $list); + } + assertType('non-empty-list', $list); + + if (count($list, COUNT_NORMAL) > 2) { + assertType('non-empty-list&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list); + assertType('int<3, max>', count($list, COUNT_NORMAL)); + } else { + assertType('non-empty-list', $list); + } + + assertType('non-empty-list', $list); + if (count($list, COUNT_RECURSIVE) > 2) { // COUNT_RECURSIVE on non-recursive array + assertType('non-empty-list&hasOffsetValue(1, int)&hasOffsetValue(2, int)', $list); + assertType('int<3, max>', count($list, COUNT_RECURSIVE)); + } else { + assertType('non-empty-list', $list); + } + } + + /** @param list $list */ + public function doFoo($list): void + { + if (count($list) === 0) { + return; + } + + if (count($list) >= 2) { + assertType('non-empty-list&hasOffsetValue(1, int)', $list); + assertType('int<2, max>', count($list)); + } else { + assertType('non-empty-list', $list); + } + } + + /** @param list $list */ + public function doBar($list): void + { + if (count($list) === 0) { + return; + } + + if (2 <= count($list)) { + assertType('non-empty-list&hasOffsetValue(1, int)', $list); + assertType('int<2, max>', count($list)); + } else { + assertType('non-empty-list', $list); + assertType('1', count($list)); + } + } + + /** @param list $list */ + public function checkLimit($list): void + { + if (count($list) === 0) { + return; + } + + if (count($list) > 9) { + assertType('non-empty-list&hasOffsetValue(1, int)&hasOffsetValue(2, int)&hasOffsetValue(3, int)&hasOffsetValue(4, int)&hasOffsetValue(5, int)&hasOffsetValue(6, int)&hasOffsetValue(7, int)&hasOffsetValue(8, int)&hasOffsetValue(9, int)', $list); + assertType('int<10, max>', count($list)); + } else { + assertType('non-empty-list', $list); + } + + if (count($list) > 10) { + assertType('non-empty-list&hasOffsetValue(1, int)&hasOffsetValue(2, int)&hasOffsetValue(3, int)&hasOffsetValue(4, int)&hasOffsetValue(5, int)&hasOffsetValue(6, int)&hasOffsetValue(7, int)&hasOffsetValue(8, int)&hasOffsetValue(9, int)', $list); + assertType('int<11, max>', count($list)); + } else { + assertType('non-empty-list', $list); + } + + } +}