From 98afed69c88563777b7422dea85a6734254f7d19 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 4 Nov 2022 03:30:21 +0100 Subject: [PATCH 1/5] Improve range computation --- .../InitializerExprTypeResolver.php | 112 ++++++++++++------ .../Analyser/data/integer-range-types.php | 42 ++++--- 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 9b2b36fb59..d32eadc821 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -65,8 +65,10 @@ use PHPStan\Type\UnionType; use function array_keys; use function array_merge; +use function ceil; use function count; use function dirname; +use function floor; use function in_array; use function is_float; use function is_int; @@ -1580,6 +1582,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 */ @@ -1645,63 +1655,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/data/integer-range-types.php b/tests/PHPStan/Analyser/data/integer-range-types.php index 0621c58689..630cd686ba 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); + } + } From de033dffe73bc9667352b10bd66fb68f5c096778 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 4 Nov 2022 04:03:21 +0100 Subject: [PATCH 2/5] Fix tests --- .../InitializerExprTypeResolver.php | 19 +++++++++++++++---- tests/PHPStan/Analyser/data/div-by-zero.php | 6 +++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index d32eadc821..990425890f 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -65,6 +65,7 @@ use PHPStan\Type\UnionType; use function array_keys; use function array_merge; +use function assert; use function ceil; use function count; use function dirname; @@ -1682,9 +1683,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T ($operandMin < 0 || $operandMin === null) && ($operandMax > 0 || $operandMax === null) ) { + $negativeOperand = IntegerRangeType::fromInterval($operandMin, 0); + assert($negativeOperand instanceof IntegerRangeType); + $positiveOperand = IntegerRangeType::fromInterval(0, $operandMax); + assert($positiveOperand instanceof IntegerRangeType); + $result = TypeCombinator::union( - $this->integerRangeMath($range, $node, IntegerRangeType::fromInterval($operandMin, 0)), - $this->integerRangeMath($range, $node, IntegerRangeType::fromInterval(0, $operandMax)) + $this->integerRangeMath($range, $node, $negativeOperand), + $this->integerRangeMath($range, $node, $positiveOperand) )->toNumber(); if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { @@ -1697,9 +1703,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T ($rangeMin < 0 || $rangeMin === null) && ($rangeMax > 0 || $rangeMax === null) ) { + $negativeRange = IntegerRangeType::fromInterval($rangeMin, 0); + assert($negativeRange instanceof IntegerRangeType); + $positiveRange = IntegerRangeType::fromInterval(0, $rangeMax); + assert($positiveRange instanceof IntegerRangeType); + $result = TypeCombinator::union( - $this->integerRangeMath(IntegerRangeType::fromInterval($rangeMin, 0), $node, $operand), - $this->integerRangeMath(IntegerRangeType::fromInterval(0, $rangeMax), $node, $operand) + $this->integerRangeMath($negativeRange, $node, $operand), + $this->integerRangeMath($positiveRange, $node, $operand) )->toNumber(); if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { diff --git a/tests/PHPStan/Analyser/data/div-by-zero.php b/tests/PHPStan/Analyser/data/div-by-zero.php index 2dae4d4767..ad027888bc 100644 --- a/tests/PHPStan/Analyser/data/div-by-zero.php +++ b/tests/PHPStan/Analyser/data/div-by-zero.php @@ -13,9 +13,9 @@ class Foo */ public function doFoo(int $range1, int $range2, int $int): void { - assertType('(float|int)', 5 / $range1); - assertType('(float|int)', 5 / $range2); - assertType('(float|int)', $range1 / $range2); + assertType('float|int<1, 5>', 5 / $range1); + assertType('float|int<-5, -1>', 5 / $range2); + assertType('float|int', $range1 / $range2); assertType('(float|int)', 5 / $int); assertType('*ERROR*', 5 / 0); From 228217eaf33003392fae796f41a69561fcbd85c2 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 4 Nov 2022 09:23:09 +0100 Subject: [PATCH 3/5] Avoid issue with multiplication by 0 --- src/Reflection/InitializerExprTypeResolver.php | 8 ++++---- tests/PHPStan/Analyser/data/integer-range-types.php | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 990425890f..bc5e9cffcb 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -1656,10 +1656,10 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T } } } elseif ($node instanceof Expr\BinaryOp\Mul) { - $min1 = ($rangeMin ?? -INF) * ($operandMin ?? -INF); - $min2 = ($rangeMin ?? -INF) * ($operandMax ?? INF); - $max1 = ($rangeMax ?? INF) * ($operandMin ?? -INF); - $max2 = ($rangeMax ?? INF) * ($operandMax ?? INF); + $min1 = $rangeMin === 0 || $operandMin === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMin ?? -INF); + $min2 = $rangeMin === 0 || $operandMax === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMax ?? INF); + $max1 = $rangeMax === 0 || $operandMin === 0 ? 0 : ($rangeMax ?? INF) * ($operandMin ?? -INF); + $max2 = $rangeMax === 0 || $operandMax === 0 ? 0 : ($rangeMax ?? INF) * ($operandMax ?? INF); $min = min($min1, $min2, $max1, $max2); $max = max($min1, $min2, $max1, $max2); diff --git a/tests/PHPStan/Analyser/data/integer-range-types.php b/tests/PHPStan/Analyser/data/integer-range-types.php index 630cd686ba..1ab38afe13 100644 --- a/tests/PHPStan/Analyser/data/integer-range-types.php +++ b/tests/PHPStan/Analyser/data/integer-range-types.php @@ -342,4 +342,16 @@ public function sayHello($p, $u): void assertType('float|int<-2, 2>', $p / $u); } + /** + * @param int<0, max> $positive + * @param int $negative + */ + public function zeroIssues($positive, $negative) + { + assertType('0', 0 * $positive); + assertType('int<0, max>', $positive * $positive); + assertType('0', 0 * $negative); + assertType('int<0, max>', $negative * $negative); + } + } From 0d875e6e0ae570d6f123768029bb9062b2be9327 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Fri, 4 Nov 2022 09:55:24 +0100 Subject: [PATCH 4/5] Add test --- tests/PHPStan/Analyser/data/integer-range-types.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Analyser/data/integer-range-types.php b/tests/PHPStan/Analyser/data/integer-range-types.php index 1ab38afe13..5e8706f84f 100644 --- a/tests/PHPStan/Analyser/data/integer-range-types.php +++ b/tests/PHPStan/Analyser/data/integer-range-types.php @@ -352,6 +352,7 @@ public function zeroIssues($positive, $negative) assertType('int<0, max>', $positive * $positive); assertType('0', 0 * $negative); assertType('int<0, max>', $negative * $negative); + assertType('int', $negative * $positive); } } From bab5bf016b4719e4a96ed0ba40cd24710bf0e98b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sun, 6 Nov 2022 12:44:08 +0100 Subject: [PATCH 5/5] Fix cs --- src/Reflection/InitializerExprTypeResolver.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index bc5e9cffcb..14ba9a421c 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -74,8 +74,10 @@ use function is_float; use function is_int; use function max; +use function min; use function sprintf; use function strtolower; +use const INF; class InitializerExprTypeResolver { @@ -1690,14 +1692,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $result = TypeCombinator::union( $this->integerRangeMath($range, $node, $negativeOperand), - $this->integerRangeMath($range, $node, $positiveOperand) + $this->integerRangeMath($range, $node, $positiveOperand), )->toNumber(); if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { return new BenevolentUnionType([new IntegerType(), new FloatType()]); - } else { - return $result; } + + return $result; } if ( ($rangeMin < 0 || $rangeMin === null) @@ -1710,14 +1712,14 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T $result = TypeCombinator::union( $this->integerRangeMath($negativeRange, $node, $operand), - $this->integerRangeMath($positiveRange, $node, $operand) + $this->integerRangeMath($positiveRange, $node, $operand), )->toNumber(); if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) { return new BenevolentUnionType([new IntegerType(), new FloatType()]); - } else { - return $result; } + + return $result; } $rangeMinSign = ($rangeMin ?? -INF) <=> 0;