From 87227df2aacc617a95aaf803c22cb003c2b9faf0 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Sat, 8 Aug 2026 11:31:03 -0600 Subject: [PATCH] Simplified improvement for absint() that always returns an int With a long list of tests https://core.trac.wordpress.org/ticket/65826 --- src/wp-includes/load.php | 12 +- tests/phpunit/tests/functions/absint.php | 384 +++++++++++++++++++++++ 2 files changed, 394 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 27c58b57dd671..2e5124e3317e0 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -1461,12 +1461,20 @@ function is_multisite() { * Converts a value to non-negative integer. * * @since 2.5.0 + * @since 7.2.0 The `int` return type was added. * * @param mixed $maybeint Data you wish to have converted to a non-negative integer. * @return int A non-negative integer. */ -function absint( $maybeint ) { - return abs( (int) $maybeint ); +function absint( $maybeint ): int { + $maybeint = (int) $maybeint; + + if ( PHP_INT_MIN === $maybeint ) { + // `abs( PHP_INT_MIN )` overflows to a float, as `PHP_INT_MAX` is one less than `-PHP_INT_MIN`. + return PHP_INT_MAX; + } + + return abs( $maybeint ); } /** diff --git a/tests/phpunit/tests/functions/absint.php b/tests/phpunit/tests/functions/absint.php index 5a9edd6961077..d28619d376b99 100644 --- a/tests/phpunit/tests/functions/absint.php +++ b/tests/phpunit/tests/functions/absint.php @@ -78,4 +78,388 @@ public function data_absint() { ), ); } + + /** + * Tests non-string scalar types, null, the empty array, and float edge values. + * + * @ticket 65826 + * + * @dataProvider data_absint_other_types + * + * @param mixed $test_value Test value. + * @param int $expected_value Expected return value. + */ + public function test_absint_other_types( $test_value, int $expected_value ): void { + $this->assertSame( $expected_value, absint( $test_value ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_absint_other_types(): array { + return array( + 'null' => array( + 'test_value' => null, + 'expected_value' => 0, + ), + 'true' => array( + 'test_value' => true, + 'expected_value' => 1, + ), + 'false' => array( + 'test_value' => false, + 'expected_value' => 0, + ), + 'empty array' => array( + 'test_value' => array(), + 'expected_value' => 0, + ), + '0 int' => array( + 'test_value' => 0, + 'expected_value' => 0, + ), + '0.0 float' => array( + 'test_value' => 0.0, + 'expected_value' => 0, + ), + 'negative zero float' => array( + 'test_value' => -0.0, + 'expected_value' => 0, + ), + 'tiny positive float' => array( + 'test_value' => 1.0e-20, + 'expected_value' => 0, + ), + 'tiny negative float' => array( + 'test_value' => -1.0e-20, + 'expected_value' => 0, + ), + 'large in-range float (2^62)' => array( + 'test_value' => 4611686018427387904.0, + 'expected_value' => 4611686018427387904, + ), + 'large in-range float (-2^62)' => array( + 'test_value' => -4611686018427387904.0, + 'expected_value' => 4611686018427387904, + ), + ); + } + + /** + * Tests that an object is converted to `1`, with a notice (PHP 7) or + * warning (PHP 8) about the object to int conversion. + * + * @ticket 65826 + */ + public function test_absint_object(): void { + $error = null; + + set_error_handler( + static function ( int $errno, string $errstr ) use ( &$error ): bool { + $error = $errstr; + return true; + } + ); + $actual = absint( new stdClass() ); + restore_error_handler(); + + $this->assertSame( 1, $actual ); + $this->assertSame( 'Object of class stdClass could not be converted to int', $error ); + } + + /** + * Tests that a resource is converted to its resource ID. + * + * @ticket 65826 + */ + public function test_absint_resource(): void { + $stream = fopen( 'php://memory', 'r' ); + + $this->assertSame( (int) $stream, absint( $stream ) ); + $this->assertGreaterThan( 0, absint( $stream ) ); + + fclose( $stream ); + } + + /** + * Tests string values in the various formats PHP recognizes when + * casting a string to an integer. + * + * @ticket 65826 + * + * @dataProvider data_absint_string_values + * + * @param string $test_value Test value. + * @param int $expected_value Expected return value. + */ + public function test_absint_string_values( string $test_value, int $expected_value ): void { + $this->assertSame( $expected_value, absint( $test_value ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_absint_string_values(): array { + return array( + 'empty string' => array( + 'test_value' => '', + 'expected_value' => 0, + ), + 'whitespace only' => array( + 'test_value' => ' ', + 'expected_value' => 0, + ), + 'zero' => array( + 'test_value' => '0', + 'expected_value' => 0, + ), + 'integer' => array( + 'test_value' => '42', + 'expected_value' => 42, + ), + 'minus sign only' => array( + 'test_value' => '-', + 'expected_value' => 0, + ), + 'plus sign only' => array( + 'test_value' => '+', + 'expected_value' => 0, + ), + 'decimal point only' => array( + 'test_value' => '.', + 'expected_value' => 0, + ), + 'exponent without a coefficient' => array( + 'test_value' => 'e5', + 'expected_value' => 0, + ), + 'uppercase exponent notation' => array( + 'test_value' => '1E3', + 'expected_value' => 1000, + ), + 'explicitly positive integer' => array( + 'test_value' => '+42', + 'expected_value' => 42, + ), + 'negative integer' => array( + 'test_value' => '-42', + 'expected_value' => 42, + ), + 'negative zero' => array( + 'test_value' => '-0', + 'expected_value' => 0, + ), + 'surrounding whitespace' => array( + 'test_value' => ' 42 ', + 'expected_value' => 42, + ), + 'leading tab and newline' => array( + 'test_value' => "\t\n-7", + 'expected_value' => 7, + ), + 'float' => array( + 'test_value' => '3.7', + 'expected_value' => 3, + ), + 'negative float' => array( + 'test_value' => '-3.7', + 'expected_value' => 3, + ), + 'float without a leading digit' => array( + 'test_value' => '.5', + 'expected_value' => 0, + ), + 'exponent notation' => array( + 'test_value' => '1e3', + 'expected_value' => 1000, + ), + 'negative float with exponent' => array( + 'test_value' => '-2.5e2', + 'expected_value' => 250, + ), + 'float rounding up to the double' => array( + 'test_value' => '1.9999999999999999', + 'expected_value' => 2, + ), + 'hexadecimal' => array( + 'test_value' => '0x1A', + 'expected_value' => 0, + ), + 'leading zero' => array( + 'test_value' => '012', + 'expected_value' => 12, + ), + 'binary' => array( + 'test_value' => '0b101', + 'expected_value' => 0, + ), + 'trailing non-numeric characters' => array( + 'test_value' => '42abc', + 'expected_value' => 42, + ), + 'leading non-numeric characters' => array( + 'test_value' => 'abc42', + 'expected_value' => 0, + ), + 'thousands separator' => array( + 'test_value' => '1,000', + 'expected_value' => 1, + ), + 'underscore separator' => array( + 'test_value' => '1_000', + 'expected_value' => 1, + ), + 'digits separated by a space' => array( + 'test_value' => '9 9', + 'expected_value' => 9, + ), + 'INF as a string' => array( + 'test_value' => 'INF', + 'expected_value' => 0, + ), + 'NAN as a string' => array( + 'test_value' => 'NAN', + 'expected_value' => 0, + ), + 'non-ASCII digits' => array( + 'test_value' => '٤٢', + 'expected_value' => 0, + ), + ); + } + + /** + * Tests that values which used to make `abs()` overflow to a float + * return the closest possible integer instead of causing a fatal error. + * + * `(float) PHP_INT_MAX` is equal to the float previously returned for + * these values, so this is the most backward compatible integer result. + * + * Unlike out of range floats, whose conversion to int is platform-dependent, + * numeric strings which exceed the integer range are reliably saturated to + * `PHP_INT_MIN` / `PHP_INT_MAX` by the string to int conversion, and strings + * which exceed the float range become `INF`, which converts to `0`. + * + * @ticket 65826 + * + * @dataProvider data_absint_extreme_values + * + * @param mixed $test_value Test value. + * @param int $expected_value Expected return value. + */ + public function test_absint_extreme_values( $test_value, int $expected_value ): void { + $this->assertSame( $expected_value, absint( $test_value ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_absint_extreme_values(): array { + return array( + 'PHP_INT_MAX' => array( + 'test_value' => PHP_INT_MAX, + 'expected_value' => PHP_INT_MAX, + ), + 'PHP_INT_MIN' => array( + 'test_value' => PHP_INT_MIN, + 'expected_value' => PHP_INT_MAX, + ), + 'PHP_INT_MIN as a float' => array( + 'test_value' => (float) PHP_INT_MIN, + 'expected_value' => PHP_INT_MAX, + ), + 'PHP_INT_MIN + 1' => array( + 'test_value' => PHP_INT_MIN + 1, + 'expected_value' => PHP_INT_MAX, + ), + 'string below the integer range' => array( + 'test_value' => '-99999999999999999999', + 'expected_value' => PHP_INT_MAX, + ), + 'string above the integer range' => array( + 'test_value' => '99999999999999999999', + 'expected_value' => PHP_INT_MAX, + ), + 'float string below the integer range' => array( + 'test_value' => '-99999999999999999999.9', + 'expected_value' => PHP_INT_MAX, + ), + 'float string above the integer range' => array( + 'test_value' => '99999999999999999999.9', + 'expected_value' => PHP_INT_MAX, + ), + 'float string just below the integer range' => array( + 'test_value' => '-9223372036854775808.5', + 'expected_value' => PHP_INT_MAX, + ), + 'float string just above the integer range' => array( + 'test_value' => '9223372036854775807.5', + 'expected_value' => PHP_INT_MAX, + ), + 'exponent string below the integer range' => array( + 'test_value' => '-1e20', + 'expected_value' => PHP_INT_MAX, + ), + 'exponent string above the integer range' => array( + 'test_value' => '1e20', + 'expected_value' => PHP_INT_MAX, + ), + 'float exponent string below the integer range' => array( + 'test_value' => '-1.5e20', + 'expected_value' => PHP_INT_MAX, + ), + 'float exponent string above the integer range' => array( + 'test_value' => '1.5e20', + 'expected_value' => PHP_INT_MAX, + ), + 'exponent string below the float range' => array( + 'test_value' => '-1e309', + 'expected_value' => 0, + ), + 'exponent string above the float range' => array( + 'test_value' => '1e309', + 'expected_value' => 0, + ), + ); + } + + /** + * Tests that an integer is returned for float values which cannot be + * represented as an integer, where the result of the `(int)` cast is + * platform-dependent. + * + * @ticket 65826 + * + * @dataProvider data_absint_unrepresentable_floats + * + * @param float $test_value Test value. + */ + public function test_absint_returns_non_negative_int_for_unrepresentable_floats( float $test_value ): void { + $actual = absint( $test_value ); + + $this->assertIsInt( $actual ); + $this->assertGreaterThanOrEqual( 0, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_absint_unrepresentable_floats(): array { + return array( + '(float) PHP_INT_MAX (2^63)' => array( (float) PHP_INT_MAX ), + '1.0e20' => array( 1.0e20 ), + '-1.0e20' => array( -1.0e20 ), + 'INF' => array( INF ), + '-INF' => array( -INF ), + 'NAN' => array( NAN ), + ); + } }