From 30d2218dbb403e5d4f71dbc39ae83cf1c8027be4 Mon Sep 17 00:00:00 2001 From: PrinsFrank <25006490+PrinsFrank@users.noreply.github.com> Date: Wed, 20 Aug 2025 21:25:45 +0200 Subject: [PATCH] Update RangeFunctionReturnTypeExtension to include known integer ranges --- .../Php/RangeFunctionReturnTypeExtension.php | 16 +++++++++----- .../Analyser/nsrt/range-array-count-limit.php | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 0df7f55f616..b9edc9de93e 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -28,6 +28,7 @@ use function abs; use function count; use function floor; +use function intdiv; use function is_array; use function is_finite; use function is_numeric; @@ -35,6 +36,7 @@ use function max; use function min; use function range; +use const PHP_INT_MAX; #[AutowiredService] final class RangeFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -186,11 +188,13 @@ private static function getLongRangeType( && $endConstant instanceof ConstantIntegerType && $stepConstant instanceof ConstantIntegerType ) { + $low = min($startConstant->getValue(), $endConstant->getValue()); + $high = max($startConstant->getValue(), $endConstant->getValue()); + return self::getNonEmptyListOfType( - IntegerRangeType::fromInterval( - min($startConstant->getValue(), $endConstant->getValue()), - max($startConstant->getValue(), $endConstant->getValue()), - ), + IntegerRangeType::fromInterval($low, $high), + // the keys stay unbounded when $high - $low overflows + $low >= 0 || $high <= PHP_INT_MAX + $low ? intdiv($high - $low, abs($stepConstant->getValue())) : null, ); } @@ -207,11 +211,11 @@ private static function getLongRangeType( ); } - private static function getNonEmptyListOfType(Type $type): IntersectionType + private static function getNonEmptyListOfType(Type $type, ?int $maxKey = null): IntersectionType { return new IntersectionType([ new ArrayType( - IntegerRangeType::createAllGreaterThanOrEqualTo(0), + IntegerRangeType::fromInterval(0, $maxKey), $type, ), new NonEmptyArrayType(), diff --git a/tests/PHPStan/Analyser/nsrt/range-array-count-limit.php b/tests/PHPStan/Analyser/nsrt/range-array-count-limit.php index ca7161f4bb3..b87a4f78807 100644 --- a/tests/PHPStan/Analyser/nsrt/range-array-count-limit.php +++ b/tests/PHPStan/Analyser/nsrt/range-array-count-limit.php @@ -13,3 +13,25 @@ function doFoo(): void assertType('non-empty-list', range(0, 100000000, 0.5)); assertType('non-empty-list>', range(0, 300)); } + +function keys(): void +{ + foreach (range(0, 50) as $key => $value) { + assertType('int<0, 50>', $key); + } + foreach (range(10, 1000, 7) as $key => $value) { + assertType('int<0, 141>', $key); + } + foreach (range(1000, 10, -7) as $key => $value) { + assertType('int<0, 141>', $key); + } + foreach (range(0, -100000000) as $key => $value) { + assertType('int<0, 100000000>', $key); + } + foreach (range(0, 2000000000, 3) as $key => $value) { + assertType('int<0, 666666666>', $key); + } + foreach (range(PHP_INT_MIN, PHP_INT_MAX) as $key => $value) { + assertType('int<0, max>', $key); + } +}