From d3e52c7b934d9e49ab275f6a141d018a8d81b61e Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 4 Nov 2022 03:30:21 +0100 Subject: [PATCH] Improve range computation --- .../InitializerExprTypeResolver.php | 112 ++++++++++++------ .../Analyser/NodeScopeResolverTest.php | 1 + .../Analyser/data/integer-range-types.php | 42 ++++--- 3 files changed, 104 insertions(+), 51 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 82638a36e01..bde2ae54c71 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -62,8 +62,10 @@ use PHPStan\Type\TypeWithClassName; use PHPStan\Type\UnionType; use function array_keys; +use function ceil; use function count; use function dirname; +use function floor; use function in_array; use function is_float; use function is_int; @@ -1551,6 +1553,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T return $union->toNumber(); } + if ($operand instanceof IntegerRangeType) { + $operandMin = $operand->getMin(); + $operandMax = $operand->getMax(); + } else { + $operandMin = $operand->getValue(); + $operandMax = $operand->getValue(); + } + if ($node instanceof BinaryOp\Plus) { if ($operand instanceof ConstantIntegerType) { /** @var int|float|null $min */ @@ -1616,63 +1626,93 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T } } } elseif ($node instanceof Expr\BinaryOp\Mul) { - if ($operand instanceof ConstantIntegerType) { - /** @var int|float|null $min */ - $min = $rangeMin !== null ? $rangeMin * $operand->getValue() : null; + $min1 = ($rangeMin ?? -INF) * ($operandMin ?? -INF); + $min2 = ($rangeMin ?? -INF) * ($operandMax ?? INF); + $max1 = ($rangeMax ?? INF) * ($operandMin ?? -INF); + $max2 = ($rangeMax ?? INF) * ($operandMax ?? INF); - /** @var int|float|null $max */ - $max = $rangeMax !== null ? $rangeMax * $operand->getValue() : null; - } else { - /** @var int|float|null $min */ - $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin * $operand->getMin() : null; + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); - /** @var int|float|null $max */ - $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax * $operand->getMax() : null; + if ($min === -INF) { + $min = null; } - - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; - } - - // invert maximas on multiplication with negative constants - if ((($range instanceof ConstantIntegerType && $range->getValue() < 0) - || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0)) - && ($min === null || $max === null)) { - [$min, $max] = [$max, $min]; + if ($max === INF) { + $max = null; } - } else { if ($operand instanceof ConstantIntegerType) { $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; } else { - $min = $rangeMin !== null && $operand->getMin() !== null && $operand->getMin() !== 0 ? $rangeMin / $operand->getMin() : null; - $max = $rangeMax !== null && $operand->getMax() !== null && $operand->getMax() !== 0 ? $rangeMax / $operand->getMax() : null; - } + // Avoid division by zero when looking for the min and the max by using the closest int + $operandMin = $operandMin !== 0 ? $operandMin : 1; + $operandMax = $operandMax !== 0 ? $operandMax : -1; + + if ( + ($operandMin < 0 || $operandMin === null) + && ($operandMax > 0 || $operandMax === null) + ) { + $result = TypeCombinator::union( + $this->integerRangeMath($range, $node, IntegerRangeType::fromInterval($operandMin, 0)), + $this->integerRangeMath($range, $node, IntegerRangeType::fromInterval(0, $operandMax)) + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } else { + return $result; + } + } + if ( + ($rangeMin < 0 || $rangeMin === null) + && ($rangeMax > 0 || $rangeMax === null) + ) { + $result = TypeCombinator::union( + $this->integerRangeMath(IntegerRangeType::fromInterval($rangeMin, 0), $node, $operand), + $this->integerRangeMath(IntegerRangeType::fromInterval(0, $rangeMax), $node, $operand) + )->toNumber(); + + if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { + return new BenevolentUnionType([new IntegerType(), new FloatType()]); + } else { + return $result; + } + } + + $rangeMinSign = ($rangeMin ?? -INF) <=> 0; + $rangeMaxSign = ($rangeMax ?? INF) <=> 0; + + $min1 = $operandMin !== null ? ($rangeMin ?? -INF) / $operandMin : $rangeMinSign * -0.1; + $min2 = $operandMax !== null ? ($rangeMin ?? -INF) / $operandMax : $rangeMinSign * 0.1; + $max1 = $operandMin !== null ? ($rangeMax ?? INF) / $operandMin : $rangeMaxSign * -0.1; + $max2 = $operandMax !== null ? ($rangeMax ?? INF) / $operandMax : $rangeMaxSign * 0.1; + + $min = min($min1, $min2, $max1, $max2); + $max = max($min1, $min2, $max1, $max2); - if ($range instanceof IntegerRangeType && $operand instanceof IntegerRangeType) { - if ($rangeMax === null && $operand->getMax() === null) { - $min = 0; - } elseif ($rangeMin === null && $operand->getMin() === null) { + if ($min === -INF) { $min = null; + } + if ($max === INF) { $max = null; } } + if ($min !== null && $max !== null && $min > $max) { + [$min, $max] = [$max, $min]; + } + if ($operand instanceof IntegerRangeType - && ($operand->getMin() === null || $operand->getMax() === null) || ($rangeMin === null || $rangeMax === null) - || is_float($min) || is_float($max) + || is_float($min) + || is_float($max) ) { if (is_float($min)) { - $min = (int) $min; + $min = (int) ceil($min); } if (is_float($max)) { - $max = (int) $max; - } - - if ($min !== null && $max !== null && $min > $max) { - [$min, $max] = [$max, $min]; + $max = (int) floor($max); } // invert maximas on division with negative constants diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0c94847fd99..ebeadd4e8f7 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -52,6 +52,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/date.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/instanceof.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); +<<<<<<< HEAD if (PHP_INT_SIZE === 8) { yield from $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); } diff --git a/tests/PHPStan/Analyser/data/integer-range-types.php b/tests/PHPStan/Analyser/data/integer-range-types.php index 0621c586899..630cd686bae 100644 --- a/tests/PHPStan/Analyser/data/integer-range-types.php +++ b/tests/PHPStan/Analyser/data/integer-range-types.php @@ -218,70 +218,70 @@ public function math($i, $j, $z, $pi, $r1, $r2, $r3, $rMin, $rMax, $x, $y) { assertType('int<2, 13>', $r1 + $j); assertType('int<-2, 9>', $r1 - $j); assertType('int<1, 30>', $r1 * $j); - assertType('float|int<0, 10>', $r1 / $j); + assertType('float|int<1, 10>', $r1 / $j); assertType('int', $rMin * $j); assertType('int<5, max>', $rMax * $j); assertType('int<2, 13>', $j + $r1); assertType('int<-9, 2>', $j - $r1); assertType('int<1, 30>', $j * $r1); - assertType('float|int<0, 3>', $j / $r1); + assertType('float|int<1, 3>', $j / $r1); assertType('int', $j * $rMin); assertType('int<5, max>', $j * $rMax); assertType('int<-19, -10>|int<2, 13>', $r1 + $z); assertType('int<-2, 9>|int<21, 30>', $r1 - $z); assertType('int<-200, -20>|int<1, 30>', $r1 * $z); - assertType('float|int<0, 10>', $r1 / $z); + assertType('float|int<1, 10>', $r1 / $z); assertType('int', $rMin * $z); assertType('int|int<5, max>', $rMax * $z); assertType('int<2, max>', $pi + 1); assertType('int<-1, max>', $pi - 2); assertType('int<2, max>', $pi * 2); - assertType('float|int<0, max>', $pi / 2); + assertType('float|int<1, max>', $pi / 2); assertType('int<2, max>', 1 + $pi); assertType('int', 2 - $pi); assertType('int<2, max>', 2 * $pi); - assertType('float|int<2, max>', 2 / $pi); + assertType('float|int<1, 2>', 2 / $pi); assertType('int<5, 14>', $r1 + 4); assertType('int<-3, 6>', $r1 - 4); assertType('int<4, 40>', $r1 * 4); - assertType('float|int<0, 2>', $r1 / 4); + assertType('float|int<1, 2>', $r1 / 4); assertType('int<9, max>', $rMax + 4); assertType('int<1, max>', $rMax - 4); assertType('int<20, max>', $rMax * 4); - assertType('float|int<1, max>', $rMax / 4); + assertType('float|int<2, max>', $rMax / 4); assertType('int<6, 20>', $r1 + $r2); assertType('int<-9, 5>', $r1 - $r2); assertType('int<5, 100>', $r1 * $r2); - assertType('float|int<0, 1>', $r1 / $r2); + assertType('float|int<1, 2>', $r1 / $r2); assertType('int<-99, 19>', $r1 - $r3); assertType('int', $r1 + $rMin); assertType('int<-4, max>', $r1 - $rMin); assertType('int', $r1 * $rMin); - assertType('float|int', $r1 / $rMin); + assertType('float|int<-10, -1>|int<1, 10>', $r1 / $rMin); assertType('int', $rMin + $r1); assertType('int', $rMin - $r1); assertType('int', $rMin * $r1); - assertType('float|int', $rMin / $r1); + assertType('float|int', $rMin / $r1); assertType('int<6, max>', $r1 + $rMax); assertType('int', $r1 - $rMax); assertType('int<5, max>', $r1 * $rMax); - assertType('float|int<0, max>', $r1 / $rMax); + assertType('float|int<1, 2>', $r1 / $rMax); assertType('int<6, max>', $rMax + $r1); assertType('int<-5, max>', $rMax - $r1); assertType('int<5, max>', $rMax * $r1); - assertType('float|int<5, max>', $rMax / $r1); + assertType('float|int<1, max>', $rMax / $r1); assertType('5|10|15|20|30', $x / $y); - assertType('float|int<0, max>', $rMax / $rMax); + assertType('float|int<1, max>', $rMax / $rMax); assertType('(float|int)', $rMin / $rMin); } @@ -307,8 +307,8 @@ public function maximaInversion($rMin, $rMax) { assertType('int<-5, max>', $rMin * -1); assertType('int', $rMax * -2); - assertType('float|int<0, max>', -1 / $rMin); - assertType('float|int', -2 / $rMax); + assertType('-1|1|float', -1 / $rMin); + assertType('float', -2 / $rMax); assertType('float|int<-5, max>', $rMin / -1); assertType('float|int', $rMax / -2); @@ -330,4 +330,16 @@ public function unaryMinus($r1, $r2, $rMin, $rMax, $rZero) { assertType('int<-50, 0>', -$rZero); } + /** + * @param int<-1, 2> $p + * @param int<-1, 2> $u + */ + public function sayHello($p, $u): void + { + assertType('int<-2, 4>', $p + $u); + assertType('int<-3, 3>', $p - $u); + assertType('int<-2, 4>', $p * $u); + assertType('float|int<-2, 2>', $p / $u); + } + }