diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 76efc63f397..28bfc7ab049 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -489,12 +489,6 @@ parameters: count: 6 path: src/Reflection/InitializerExprTypeResolver.php - - - rawMessage: PHPDoc tag @var with type float|int is not subtype of native type int. - identifier: varTag.nativeType - count: 1 - path: src/Reflection/InitializerExprTypeResolver.php - - rawMessage: PHPDoc tag @var with type float|int is not subtype of type int. identifier: varTag.type diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 7332aab8f36..d9e73843383 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -122,6 +122,7 @@ use function str_starts_with; use function strtolower; use const INF; +use const PHP_INT_MIN; #[AutowiredService] final class InitializerExprTypeResolver @@ -2607,12 +2608,21 @@ public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type return $specifiedTypes; } - $type = $this->getUnaryMinusTypeFromType($expr, $type); - if ($type instanceof IntegerRangeType) { - return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); + $negatedType = $this->getUnaryMinusTypeFromType($expr, $type); + if ($negatedType instanceof IntegerRangeType) { + $negatedType = $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1))); } - return $type; + $numberType = $type->toNumber(); + if ( + $numberType->isInteger()->yes() + && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() + ) { + // Negating the smallest integer overflows into a float. + return TypeCombinator::union($negatedType, new ConstantFloatType(-(float) PHP_INT_MIN)); + } + + return $negatedType; } public function getUnaryMinusTypeFromType(Expr $expr, Type $type): Type @@ -2624,7 +2634,6 @@ public function getUnaryMinusTypeFromType(Expr $expr, Type $type): Type $newTypes = []; foreach ($scalarValues as $scalarValue) { if (is_int($scalarValue)) { - /** @var int|float $newValue */ $newValue = -$scalarValue; if (!is_int($newValue)) { // Negating the smallest integer overflows into a float. diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index 37965c001e9..c5fa20607a6 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -17,8 +17,8 @@ use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use function abs; +use function is_int; use function sprintf; -use const PHP_INT_MIN; /** @api */ class ConstantIntegerType extends IntegerType implements ConstantScalarType @@ -86,14 +86,13 @@ public function toBitwiseNotType(): Type public function toAbsoluteNumber(): Type { - if ($this->value === PHP_INT_MIN) { + $absoluteValue = abs($this->value); + if (!is_int($absoluteValue)) { // The absolute value of the smallest integer is not representable as an int. - // Checking is_int(abs($this->value)) instead is dead code to PHPStan itself, - // which infers abs(int) as int<0, max>. - return new ConstantFloatType(-(float) $this->value); + return new ConstantFloatType($absoluteValue); } - return new self(abs($this->value)); + return new self($absoluteValue); } public function toString(): Type diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index 605507008b3..e1ec1ef6b85 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -13,6 +13,7 @@ use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Accessory\AccessoryNonFalsyStringType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use function array_filter; use function array_map; @@ -90,7 +91,9 @@ public static function createAllSmallerThan($value): Type return self::fromInterval(null, $value, -1); } - if ($value > PHP_INT_MAX) { + // (float) PHP_INT_MAX rounds up to 2 ** 63, so a float that equals it + // is already bigger than every integer. + if ($value >= (float) PHP_INT_MAX) { return new IntegerType(); } @@ -160,7 +163,9 @@ public static function createAllGreaterThanOrEqualTo($value): Type return new IntegerType(); } - if ($value > PHP_INT_MAX) { + // (float) PHP_INT_MAX rounds up to 2 ** 63, so a float that equals it + // is already bigger than every integer. + if ($value >= (float) PHP_INT_MAX) { return new NeverType(); } @@ -499,16 +504,27 @@ public function toAbsoluteNumber(): Type return $this; } - // Negating the smallest integer overflows, so its absolute value is treated as unbounded, - // the same way an unbounded lower bound is. This keeps abs(int) and - // abs(int<-9223372036854775808, 0>) in agreement. - $inversedMin = $this->min !== null && $this->min !== PHP_INT_MIN ? -$this->min : null; + // The absolute value of the smallest integer overflows into a float. + $overflowType = new ConstantFloatType(-(float) PHP_INT_MIN); + if ($this->max !== null && $this->max <= PHP_INT_MIN) { + return $overflowType; + } + + // Without the smallest integer the absolute values only reach PHP_INT_MAX, + // which is what an unbounded upper bound stands for. + $inversedMin = $this->min !== null && $this->min > PHP_INT_MIN ? -$this->min : null; if ($this->max === null || $this->max >= 0) { - return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + $absoluteRange = self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + } else { + $absoluteRange = self::fromInterval(-$this->max, $inversedMin); + } + + if ($inversedMin !== null) { + return $absoluteRange; } - return self::fromInterval(-$this->max, $inversedMin); + return TypeCombinator::union($absoluteRange, $overflowType); } public function toString(): Type diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 2ba3ad1a080..deddeef61fb 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -9,6 +9,7 @@ use PHPStan\Type\Accessory\AccessoryDecimalIntegerStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Traits\NonArrayTypeTrait; use PHPStan\Type\Traits\NonCallableTypeTrait; @@ -19,6 +20,7 @@ use PHPStan\Type\Traits\NonOffsetAccessibleTypeTrait; use PHPStan\Type\Traits\UndecidedBooleanTypeTrait; use PHPStan\Type\Traits\UndecidedComparisonTypeTrait; +use const PHP_INT_MIN; /** @api */ #[InstanceofDeprecated(insteadUse: 'Type::isInteger()')] @@ -63,7 +65,11 @@ public function toBitwiseNotType(): Type public function toAbsoluteNumber(): Type { - return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + return TypeCombinator::union( + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + // The absolute value of the smallest integer overflows into a float. + new ConstantFloatType(-(float) PHP_INT_MIN), + ); } public function toFloat(): Type diff --git a/tests/PHPStan/Analyser/Fiber/data/fnsr.php b/tests/PHPStan/Analyser/Fiber/data/fnsr.php index ccfeaa3e85a..9145276025b 100644 --- a/tests/PHPStan/Analyser/Fiber/data/fnsr.php +++ b/tests/PHPStan/Analyser/Fiber/data/fnsr.php @@ -212,8 +212,8 @@ function doUnaryMinus(int $i) { assertType('-1', -$a); assertNativeType('-1', -$a); - assertType('int', -$i); - assertNativeType('int', -$i); + assertType('9.223372036854776E+18|int', -$i); + assertNativeType('9.223372036854776E+18|int', -$i); } /** diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 24f55aad643..ff870ef81a6 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -114,6 +114,7 @@ private static function findTestFiles(): iterable yield __DIR__ . '/data/predefined-constants-64bit.php'; yield __DIR__ . '/data/abs-64bit.php'; yield __DIR__ . '/data/unary-minus-64bit.php'; + yield __DIR__ . '/data/integer-range-float-bounds-64bit.php'; } else { yield __DIR__ . '/data/predefined-constants-32bit.php'; } diff --git a/tests/PHPStan/Analyser/data/abs-64bit.php b/tests/PHPStan/Analyser/data/abs-64bit.php index c1f34ae9015..127d1b86ad6 100644 --- a/tests/PHPStan/Analyser/data/abs-64bit.php +++ b/tests/PHPStan/Analyser/data/abs-64bit.php @@ -15,13 +15,20 @@ function integerRanges(int $int): void { /** @var int<-9223372036854775808, 0> $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); /** @var int<-9223372036854775808, -1> $int */ - assertType('int<1, max>', abs($int)); + assertType('9.223372036854776E+18|int<1, max>', abs($int)); /** @var int<-9223372036854775808, 9223372036854775807> $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); + + // One step away from the overflow, so the result stays an integer. + /** @var int<-9223372036854775807, 0> $int */ + assertType('int<0, 9223372036854775807>', abs($int)); + + /** @var int<-9223372036854775807, 9223372036854775807> $int */ + assertType('int<0, 9223372036854775807>', abs($int)); // IntegerRangeType::fromInterval() collapses these to a single value. /** @var int $int */ @@ -32,3 +39,9 @@ function integerRanges(int $int): void assertType('9223372036854775807', $int); assertType('9223372036854775807', abs($int)); } + +// https://github.com/phpstan/phpstan/issues/15069 +function absint($maybeint): void +{ + assertType('9.223372036854776E+18|int<0, max>', abs((int) $maybeint)); +} diff --git a/tests/PHPStan/Analyser/data/integer-range-float-bounds-64bit.php b/tests/PHPStan/Analyser/data/integer-range-float-bounds-64bit.php new file mode 100644 index 00000000000..951e6fd0326 --- /dev/null +++ b/tests/PHPStan/Analyser/data/integer-range-float-bounds-64bit.php @@ -0,0 +1,67 @@ + 9223372036854775808) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } + + if ($i >= 9223372036854775808) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } +} + +// (float) PHP_INT_MIN is exactly PHP_INT_MIN, so nothing is smaller than it. +function belowIntegerRange(int $i): void +{ + assertType('-9.223372036854776E+18', -9223372036854775808); + + if ($i < -9223372036854775808) { + assertType('*NEVER*', $i); + } else { + assertType('int', $i); + } + + if ($i >= -9223372036854775808) { + assertType('int', $i); + } else { + assertType('*NEVER*', $i); + } +} + +// The biggest float below 2 ** 63 still fits into the integer range. +function insideIntegerRange(int $i): void +{ + if ($i < 9223372036854774784.0) { + assertType('int', $i); + } + + if ($i >= 9223372036854774784.0) { + assertType('int<9223372036854774784, max>', $i); + } +} diff --git a/tests/PHPStan/Analyser/data/unary-minus-64bit.php b/tests/PHPStan/Analyser/data/unary-minus-64bit.php index 23105acd860..3880450145b 100644 --- a/tests/PHPStan/Analyser/data/unary-minus-64bit.php +++ b/tests/PHPStan/Analyser/data/unary-minus-64bit.php @@ -17,13 +17,39 @@ assertType('9223372036854775807', -(-9223372036854775807)); assertType('-9223372036854775807', -9223372036854775807); +function unboundedIntegers(int $int, string $numericString): void +{ + // https://github.com/phpstan/phpstan/issues/15069 + assertType('9.223372036854776E+18|int', -$int); + assertType('9.223372036854776E+18|int', -((int) $numericString)); + + // Unary plus never overflows. + assertType('int', +$int); + + /** @var numeric-string $numericString */ + assertType('float|int', -$numericString); +} + function integerRanges(int $int): void { /** @var int $int */ - assertType('int<1, max>', -$int); + assertType('9.223372036854776E+18|int<1, max>', -$int); /** @var int<-9223372036854775808, -1> $int */ - assertType('int<1, max>', -$int); + assertType('9.223372036854776E+18|int<1, max>', -$int); + + /** @var int<-9223372036854775807, -1> $int */ + assertType('int<1, 9223372036854775807>', -$int); + + /** @var int $int */ + assertType('9.223372036854776E+18|int<-5, max>', -$int); + + // The lower bound is known, so the overflow cannot happen. + /** @var int<0, max> $int */ + assertType('int', -$int); + + /** @var int<-5, 5> $int */ + assertType('int<-5, 5>', -$int); } function constantUnion(int $int): void diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php index 506f436c02e..3da21a3431f 100644 --- a/tests/PHPStan/Analyser/nsrt/abs.php +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -12,19 +12,19 @@ class Foo public function singleIntegerRange(int $int): void { /** @var int $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); /** @var positive-int $int */ assertType('int<1, max>', abs($int)); /** @var negative-int $int */ - assertType('int<1, max>', abs($int)); + assertType('9.223372036854776E+18|int<1, max>', abs($int)); /** @var non-negative-int $int */ assertType('int<0, max>', abs($int)); /** @var non-positive-int $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); /** @var int<0, max> $int */ assertType('int<0, max>', abs($int)); @@ -45,13 +45,13 @@ public function singleIntegerRange(int $int): void assertType('int<123, 456>', abs($int)); /** @var int $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); /** @var int $int */ - assertType('int<1, max>', abs($int)); + assertType('9.223372036854776E+18|int<1, max>', abs($int)); /** @var int $int */ - assertType('int<123, max>', abs($int)); + assertType('9.223372036854776E+18|int<123, max>', abs($int)); /** @var int<-456, -123> $int */ assertType('int<123, 456>', abs($int)); @@ -60,16 +60,16 @@ public function singleIntegerRange(int $int): void assertType('int<0, 123>', abs($int)); /** @var int $int */ - assertType('int<0, max>', abs($int)); + assertType('9.223372036854776E+18|int<0, max>', abs($int)); } public function multipleIntegerRanges(int $int): void { /** @var non-zero-int $int */ - assertType('int<1, max>', abs($int)); + assertType('9.223372036854776E+18|int<1, max>', abs($int)); /** @var int|int<1, max> $int */ - assertType('int<1, max>', abs($int)); + assertType('9.223372036854776E+18|int<1, max>', abs($int)); /** @var int<-20, -10>|int<5, 25> $int */ assertType('int<5, 25>', abs($int)); @@ -106,7 +106,7 @@ public function mixedIntegerUnion(int $int): void assertType('123|int<456, max>', abs($int)); /** @var int|-123 $int */ - assertType('123|int<456, max>', abs($int)); + assertType('123|9.223372036854776E+18|int<456, max>', abs($int)); /** @var -123|int<124, 125> $int */ assertType('int<123, 125>', abs($int)); diff --git a/tests/PHPStan/Analyser/nsrt/binary.php b/tests/PHPStan/Analyser/nsrt/binary.php index ce3a13e6cc6..d29b831c880 100644 --- a/tests/PHPStan/Analyser/nsrt/binary.php +++ b/tests/PHPStan/Analyser/nsrt/binary.php @@ -273,7 +273,7 @@ public function doFoo(array $generalArray) assertType('*ERROR*', -"blabla"); assertType('-5', -5); assertType('5', -(-5)); - assertType('int', -$integer); + assertType('9.223372036854776E+18|int', -$integer); assertType('-2|-1', -$conditionalInt); assertType('*ERROR*', -$string); assertType('2', 1 + 1); diff --git a/tests/PHPStan/Analyser/nsrt/bug-9224b.php b/tests/PHPStan/Analyser/nsrt/bug-9224b.php index 863d17cb85e..a7a637d312c 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9224b.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9224b.php @@ -10,8 +10,8 @@ class Foo /** @param array $arr */ public function sayHello(array $arr): void { - assertType('array>', array_map('abs', $arr)); - assertType('array>', array_map(abs(...), $arr)); + assertType('array<9.223372036854776E+18|int<0, max>>', array_map('abs', $arr)); + assertType('array<9.223372036854776E+18|int<0, max>>', array_map(abs(...), $arr)); } } diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index 8adf56f1c10..032d35dafdf 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -329,7 +329,7 @@ public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { assertType('int<-10, -1>', -$r1); assertType('int<-10, 5>', -$r2); - assertType('int<-5, max>', -$rMin); + assertType('9.223372036854776E+18|int<-5, max>', -$rMin); assertType('int', -$rMax); assertType('int<-50, 0>', -$rZero); } diff --git a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php index c9463f9675a..fdaacd9e139 100644 --- a/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php +++ b/tests/PHPStan/Command/ErrorFormatter/BaselineNeonErrorFormatterTest.php @@ -383,7 +383,7 @@ public function testOutputOrdering(array $errors): void } /** - * @return Generator, existingBaselineContent: string, expectedNewlinesCount: int}> + * @return Generator, existingBaselineContent: string, expectedNewlinesCount: int<0, max>}> */ public static function endOfFileNewlinesProvider(): Generator { @@ -467,6 +467,7 @@ public static function endOfFileNewlinesProvider(): Generator /** * @param list $errors + * @param int<0, max> $expectedNewlinesCount */ #[DataProvider('endOfFileNewlinesProvider')] public function testEndOfFileNewlines( diff --git a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php index 88a4b5562cf..7a567832efe 100644 --- a/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php +++ b/tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php @@ -481,6 +481,22 @@ public function testBug13190(): void ]); } + public function testBug15069(): void + { + $this->checkNullables = true; + $this->checkExplicitMixed = false; + $this->analyse([__DIR__ . '/data/bug-15069.php'], [ + [ + 'Function Bug15069\absint() should return int but returns float|int<0, max>.', + 10, + ], + [ + 'Function Bug15069\negate() should return int but returns float|int.', + 24, + ], + ]); + } + public function testBug13114(): void { $this->checkNullables = true; diff --git a/tests/PHPStan/Rules/Functions/data/bug-15069.php b/tests/PHPStan/Rules/Functions/data/bug-15069.php new file mode 100644 index 00000000000..45fb29cbca1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/bug-15069.php @@ -0,0 +1,33 @@ + $int + */ +function negateBoundedRange(int $int): int +{ + return -$int; +}