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
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 14 additions & 5 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
use function str_starts_with;
use function strtolower;
use const INF;
use const PHP_INT_MIN;

#[AutowiredService]
final class InitializerExprTypeResolver
Expand Down Expand Up @@ -2607,12 +2608,21 @@
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
Expand All @@ -2624,7 +2634,6 @@
$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.
Expand All @@ -2650,8 +2659,8 @@
{
$exprType = $getTypeCallback($expr);

$specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistry

Check warning on line 2662 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $numberType = $type->toNumber(); if ( - $numberType->isInteger()->yes() + !$numberType->isInteger()->no() && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() ) { // Negating the smallest integer overflows into a float.

Check warning on line 2662 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $numberType = $type->toNumber(); if ( - $numberType->isInteger()->yes() + !$numberType->isInteger()->no() && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() ) { // Negating the smallest integer overflows into a float.
->callUnaryOperatorTypeSpecifyingExtensions('~', $exprType);

Check warning on line 2663 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ $numberType = $type->toNumber(); if ( $numberType->isInteger()->yes() - && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() + && !$numberType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN))->no() ) { // Negating the smallest integer overflows into a float. return TypeCombinator::union($negatedType, new ConstantFloatType(-(float) PHP_INT_MIN));

Check warning on line 2663 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $numberType = $type->toNumber(); if ( $numberType->isInteger()->yes() - && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() + && (new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->yes() ) { // Negating the smallest integer overflows into a float. return TypeCombinator::union($negatedType, new ConstantFloatType(-(float) PHP_INT_MIN));

Check warning on line 2663 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $numberType = $type->toNumber(); if ( $numberType->isInteger()->yes() - && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() + && (new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->yes() ) { // Negating the smallest integer overflows into a float. return TypeCombinator::union($negatedType, new ConstantFloatType(-(float) PHP_INT_MIN));

Check warning on line 2663 in src/Reflection/InitializerExprTypeResolver.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ $numberType = $type->toNumber(); if ( $numberType->isInteger()->yes() - && !(new ConstantIntegerType(PHP_INT_MIN))->isSuperTypeOf($numberType)->no() + && !$numberType->isSuperTypeOf(new ConstantIntegerType(PHP_INT_MIN))->no() ) { // Negating the smallest integer overflows into a float. return TypeCombinator::union($negatedType, new ConstantFloatType(-(float) PHP_INT_MIN));
if ($specifiedTypes !== null) {
return $specifiedTypes;
}
Expand Down
11 changes: 5 additions & 6 deletions src/Type/Constant/ConstantIntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
32 changes: 24 additions & 8 deletions src/Type/IntegerRangeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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<min, 0>) 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
Expand Down
8 changes: 7 additions & 1 deletion src/Type/IntegerType.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()')]
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/Fiber/data/fnsr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
Expand Down
19 changes: 16 additions & 3 deletions tests/PHPStan/Analyser/data/abs-64bit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<min, -9223372036854775808> $int */
Expand All @@ -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));
}
67 changes: 67 additions & 0 deletions tests/PHPStan/Analyser/data/integer-range-float-bounds-64bit.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php declare(strict_types = 1);

namespace IntegerRangeFloatBounds;

use function PHPStan\Testing\assertType;

// 9223372036854775808 does not fit into an int, so it is a float literal.
// It is the same float as (float) PHP_INT_MAX, which rounds up to 2 ** 63,
// and as the abs()/unary minus overflow of PHP_INT_MIN.
function aboveIntegerRange(int $i): void
{
assertType('9.223372036854776E+18', 9223372036854775808);

if ($i < 9223372036854775808) {
assertType('int', $i);
} else {
assertType('*NEVER*', $i);
}

if ($i <= 9223372036854775808) {
assertType('int', $i);
} else {
assertType('*NEVER*', $i);
}

if ($i > 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<min, 9223372036854774783>', $i);
}

if ($i >= 9223372036854774784.0) {
assertType('int<9223372036854774784, max>', $i);
}
}
30 changes: 28 additions & 2 deletions tests/PHPStan/Analyser/data/unary-minus-64bit.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<min, -1> $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<min, 5> $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<min, 0>', -$int);

/** @var int<-5, 5> $int */
assertType('int<-5, 5>', -$int);
}

function constantUnion(int $int): void
Expand Down
Loading
Loading