Skip to content

Commit 47d246f

Browse files
zonuexeclaude
andcommitted
Follow the float step of PHP 8.3 in character ranges
PHP 8.3 keeps a step with a fractional part, or one beyond the integer range such as -1e20, as a float and then compares numbers, in which a digit keeps its value and any other character is 0: range('a', '9', -0.5) throws, range('a', 'z', -1e20) returns [0.0]. is_numeric() on PHP 7.4 rejects a numeric string with trailing whitespace, which PHP 8 treats as a number, so the helper strips it. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d786000 commit 47d246f

2 files changed

Lines changed: 29 additions & 9 deletions

File tree

‎src/Type/Php/RangeFunctionArgumentsHelper.php‎

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use function is_string;
1515
use function ord;
1616
use function range;
17+
use function rtrim;
1718
use function strlen;
1819
use const PHP_INT_MIN;
1920
use const PHP_VERSION_ID;
@@ -29,6 +30,9 @@ final class RangeFunctionArgumentsHelper
2930
/** 2 ** 53, above which a float no longer holds every integer */
3031
private const PRECISE_INTEGER_LIMIT = 9007199254740992;
3132

33+
/** 2 ** 63, from which PHP 8.3 keeps an integral float step as a float */
34+
private const INTEGER_STEP_LIMIT = 9.2233720368547758E18;
35+
3236
/**
3337
* Calls range() on the PHP version PHPStan itself runs on.
3438
*
@@ -137,12 +141,10 @@ public static function isNegativeStepOnIncreasingRange(int|float|string $start,
137141
return false;
138142
}
139143

140-
if (self::isCharacter($start) && self::isCharacter($end)) {
141-
// a step with a fractional part turns both characters into 0
142-
if (is_float($step) && floor($step) !== $step) {
143-
return false;
144-
}
145-
144+
// PHP 8.3 keeps a step with a fractional part or beyond the integer range as a float,
145+
// and then compares numbers, in which only a digit keeps its value
146+
$isFloatStep = is_float($step) && (floor($step) !== $step || abs($step) >= self::INTEGER_STEP_LIMIT);
147+
if (!$isFloatStep && self::isCharacter($start) && self::isCharacter($end)) {
146148
return ord($start[0]) < ord($end[0]);
147149
}
148150

@@ -158,7 +160,15 @@ public static function isNegativeStepOnIncreasingRange(int|float|string $start,
158160
*/
159161
private static function isCharacter(int|float|string $value): bool
160162
{
161-
return is_string($value) && $value !== '' && (strlen($value) === 1 || !is_numeric($value));
163+
return is_string($value) && $value !== '' && (strlen($value) === 1 || !self::isNumeric($value));
164+
}
165+
166+
/**
167+
* PHP 8 accepts whitespace after a numeric string, which is_numeric() on PHP 7.4 does not.
168+
*/
169+
private static function isNumeric(int|float|string $value): bool
170+
{
171+
return is_numeric($value) || (is_string($value) && is_numeric(rtrim($value, " \t\n\r\v\f")));
162172
}
163173

164174
private static function isImprecise(int|float|string $value): bool
@@ -168,7 +178,7 @@ private static function isImprecise(int|float|string $value): bool
168178

169179
private static function toNumber(int|float|string $value): float
170180
{
171-
return is_numeric($value) ? (float) $value : 0.0;
181+
return self::isNumeric($value) ? (float) $value : 0.0;
172182
}
173183

174184
}

‎tests/PHPStan/Type/Php/RangeFunctionArgumentsHelperTest.php‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,18 @@ public static function dataIsNegativeStepOnIncreasingRange(): iterable
2727
yield ['ab', 'z', -1, true];
2828
yield ['a', 'z', -1.0, true];
2929

30-
// a step with a fractional part turns both characters into 0
30+
// PHP 8.3 keeps a step with a fractional part or beyond the integer range as a float,
31+
// which turns a character into 0 unless it is a digit
3132
yield ['a', 'z', -0.5, false];
33+
yield ['0', '9', -0.0001, true];
34+
yield ['a', '9', -0.0001, true];
35+
yield ['9', 'a', -0.0001, false];
36+
yield ['a', 'z', -1e20, false];
37+
yield ['a', '9', -1e20, true];
38+
39+
// a numeric string with whitespace around it is a number, also on PHP 7.4
40+
yield ['5 ', 3, -1, false];
41+
yield [' 5', 3, -1, false];
3242

3343
// an empty string or a character next to a number is 0
3444
yield ['', 'a', -1, false];

0 commit comments

Comments
 (0)