Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
class IntegerRangeType extends IntegerType implements CompoundType
{

/** The first float above PHP_INT_MAX: 2^63 on 64-bit builds, 2^31 on 32-bit ones, exact on both. */
private const FLOAT_ABOVE_INT_MAX = PHP_INT_MAX + 1.0;

private function __construct(private ?int $min, private ?int $max)
{
parent::__construct();
Expand Down Expand Up @@ -90,15 +93,19 @@ public static function createAllSmallerThan($value): Type
return self::fromInterval(null, $value, -1);
}

if ($value > PHP_INT_MAX) {
// decided on the ceil() about to be cast: comparing $value with PHP_INT_MAX
// converts PHP_INT_MAX to float, which rounds it up to 2^63 on 64-bit builds
// but keeps it exact on 32-bit ones, where 2147483647.0 is still in range
$ceil = ceil($value);
if ($ceil >= self::FLOAT_ABOVE_INT_MAX) {
return new IntegerType();
}

if ($value <= PHP_INT_MIN) {
return new NeverType();
}

return self::fromInterval(null, (int) ceil($value), -1);
return self::fromInterval(null, (int) $ceil, -1);
}

/**
Expand Down Expand Up @@ -160,11 +167,13 @@ public static function createAllGreaterThanOrEqualTo($value): Type
return new IntegerType();
}

if ($value > PHP_INT_MAX) {
// decided on the ceil() about to be cast, like createAllSmallerThan()
$ceil = ceil($value);
if ($ceil >= self::FLOAT_ABOVE_INT_MAX) {
return new NeverType();
}

return self::fromInterval((int) ceil($value), null);
return self::fromInterval((int) $ceil, null);
}

public function getMin(): ?int
Expand Down
43 changes: 43 additions & 0 deletions tests/PHPStan/Analyser/nsrt/integer-range-float-two-to-the-63.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php declare(strict_types = 1);

namespace IntegerRangeFloatTwoToThe63;

use function PHPStan\Testing\assertType;

function smaller(int $i): void
{
if ($i < 9.2233720368547758E18) {
assertType('int', $i);
} else {
assertType('*NEVER*', $i);
}
assertType('int', $i);
}

function greaterOrEqual(int $i): void
{
if ($i >= 9.2233720368547758E18) {
assertType('*NEVER*', $i);
} else {
assertType('int', $i);
}
assertType('int', $i);
}

function smallerOrEqual(int $i): void
{
if ($i <= 9.2233720368547758E18) {
assertType('int', $i);
} else {
assertType('*NEVER*', $i);
}
}

function greater(int $i): void
{
if ($i > 9.2233720368547758E18) {
assertType('*NEVER*', $i);
} else {
assertType('int', $i);
}
}
136 changes: 136 additions & 0 deletions tests/PHPStan/Type/IntegerRangeTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Testing\PHPStanTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use RuntimeException;
use function restore_error_handler;
use function set_error_handler;
use const PHP_INT_MAX;
use const PHP_INT_MIN;
use const PHP_INT_SIZE;

class IntegerRangeTypeTest extends PHPStanTestCase
{

public static function dataCreateFromFloat(): iterable
{
// 2**63 is PHP_INT_MAX + 1, the smallest float greater than every int
yield '2**63' => [
9.2233720368547758E18,
'int',
'int',
'*NEVER*',
'*NEVER*',
];

// the largest float smaller than 2**63
yield '2**63 - 1024' => [
9.2233720368547748E18,
'int<min, 9223372036854774783>',
'int<min, 9223372036854774784>',
'int<9223372036854774785, max>',
'int<9223372036854774784, max>',
];

// -2**63 is exactly PHP_INT_MIN
yield '-2**63' => [
-9.2233720368547758E18,
'*NEVER*',
'-9223372036854775808',
'int<-9223372036854775807, max>',
'int',
];

// the largest float smaller than -2**63
yield '-2**63 - 2048' => [
-9.2233720368547779E18,
'*NEVER*',
'*NEVER*',
'int',
'int',
];
}

#[DataProvider('dataCreateFromFloat')]
public function testCreateFromFloat(
float $value,
string $expectedSmallerThan,
string $expectedSmallerThanOrEqualTo,
string $expectedGreaterThan,
string $expectedGreaterThanOrEqualTo,
): void
{
if (PHP_INT_SIZE !== 8) {
$this->markTestSkipped('The bounds are those of a 64-bit int.');
}

$this->assertSame([
'createAllSmallerThan' => $expectedSmallerThan,
'createAllSmallerThanOrEqualTo' => $expectedSmallerThanOrEqualTo,
'createAllGreaterThan' => $expectedGreaterThan,
'createAllGreaterThanOrEqualTo' => $expectedGreaterThanOrEqualTo,
], [
'createAllSmallerThan' => IntegerRangeType::createAllSmallerThan($value)->describe(VerbosityLevel::precise()),
'createAllSmallerThanOrEqualTo' => IntegerRangeType::createAllSmallerThanOrEqualTo($value)->describe(VerbosityLevel::precise()),
'createAllGreaterThan' => IntegerRangeType::createAllGreaterThan($value)->describe(VerbosityLevel::precise()),
'createAllGreaterThanOrEqualTo' => IntegerRangeType::createAllGreaterThanOrEqualTo($value)->describe(VerbosityLevel::precise()),
]);
}

public static function dataFloatBounds(): iterable
{
$smallerThan = static fn (float $value): Type => IntegerRangeType::createAllSmallerThan($value);
$smallerThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllSmallerThanOrEqualTo($value);
$greaterThan = static fn (float $value): Type => IntegerRangeType::createAllGreaterThan($value);
$greaterThanOrEqualTo = static fn (float $value): Type => IntegerRangeType::createAllGreaterThanOrEqualTo($value);

// PHP_INT_MAX + 1.0 is the first float above PHP_INT_MAX on any int width, and
// (float) PHP_INT_MIN is exact on any int width
$aboveMax = PHP_INT_MAX + 1.0;
yield [$smallerThan, $aboveMax, 'int'];
yield [$smallerThanOrEqualTo, $aboveMax, 'int'];
yield [$greaterThan, $aboveMax, '*NEVER*'];
yield [$greaterThanOrEqualTo, $aboveMax, '*NEVER*'];
yield [$smallerThan, (float) PHP_INT_MIN, '*NEVER*'];
yield [$smallerThanOrEqualTo, (float) PHP_INT_MIN, (string) PHP_INT_MIN];
yield [$greaterThan, (float) PHP_INT_MIN, 'int<' . (PHP_INT_MIN + 1) . ', max>'];
yield [$greaterThanOrEqualTo, (float) PHP_INT_MIN, 'int'];

// (float) PHP_INT_MAX is 2^63 on 64-bit builds (above every int) and exact on 32-bit
// ones; so is PHP_INT_MAX - 0.5, which is PHP_INT_MAX - 1 + 0.5 on 32-bit builds
$maxIsExact = PHP_INT_SIZE < 8;
yield [$smallerThan, (float) PHP_INT_MAX, $maxIsExact ? 'int<min, ' . (PHP_INT_MAX - 1) . '>' : 'int'];
yield [$smallerThanOrEqualTo, (float) PHP_INT_MAX, 'int'];
yield [$greaterThan, (float) PHP_INT_MAX, '*NEVER*'];
yield [$greaterThanOrEqualTo, (float) PHP_INT_MAX, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*'];
yield [$smallerThan, PHP_INT_MAX - 0.5, $maxIsExact ? 'int<min, ' . (PHP_INT_MAX - 1) . '>' : 'int'];
yield [$greaterThanOrEqualTo, PHP_INT_MAX - 0.5, $maxIsExact ? (string) PHP_INT_MAX : '*NEVER*'];

// between PHP_INT_MAX and the next int on 32-bit builds, 2^63 on 64-bit ones: its
// ceil() is past the int range either way
yield [$smallerThan, PHP_INT_MAX + 0.5, 'int'];
yield [$greaterThanOrEqualTo, PHP_INT_MAX + 0.5, '*NEVER*'];

yield [$smallerThan, 2.5, 'int<min, 2>'];
yield [$greaterThan, 2.5, 'int<3, max>'];
}

/**
* @param callable(float): Type $factory
*/
#[DataProvider('dataFloatBounds')]
public function testFloatBounds(callable $factory, float $value, string $expected): void
{
set_error_handler(static function (int $errno, string $errstr): bool {
throw new RuntimeException($errstr);
});
try {
$this->assertSame($expected, $factory($value)->describe(VerbosityLevel::precise()));
} finally {
restore_error_handler();
}
}

}
Loading