From dd534eaac936d768eb4849452681f09c258ef381 Mon Sep 17 00:00:00 2001 From: Giovanni Giacobbi Date: Mon, 10 Jun 2024 21:53:32 +0200 Subject: [PATCH] Implement bit shift operation on integers and unions --- phpstan-baseline.neon | 10 --- .../InitializerExprTypeResolver.php | 38 ++++++-- .../Analyser/nsrt/integer-range-types.php | 86 +++++++++++++++++++ 3 files changed, 117 insertions(+), 17 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index bc9976e16b..994c97095f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -360,16 +360,6 @@ parameters: count: 1 path: src/Reflection/InitializerExprTypeResolver.php - - - message: "#^Binary operation \"\\<\\<\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\<0, max\\>\\|string\\|null results in an error\\.$#" - count: 1 - path: src/Reflection/InitializerExprTypeResolver.php - - - - message: "#^Binary operation \"\\>\\>\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\<0, max\\>\\|string\\|null results in an error\\.$#" - count: 1 - path: src/Reflection/InitializerExprTypeResolver.php - - message: "#^Binary operation \"\\^\" between bool\\|float\\|int\\|string\\|null and bool\\|float\\|int\\|string\\|null results in an error\\.$#" count: 1 diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 875df63b9b..b9e4415f99 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -77,6 +77,7 @@ use function dirname; use function floor; use function in_array; +use function intval; use function is_finite; use function is_float; use function is_int; @@ -1255,7 +1256,7 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb return new ErrorType(); } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() << $rightNumberType->getValue()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue())); if ($generalize) { $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); } @@ -1273,7 +1274,7 @@ public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallb return new ErrorType(); } - return new IntegerType(); + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftLeft($left, $right), $leftType, $rightType); } /** @@ -1312,7 +1313,7 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return new ErrorType(); } - $resultType = $this->getTypeFromValue($leftNumberType->getValue() >> $rightNumberType->getValue()); + $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue())); if ($generalize) { $resultType = $resultType->generalize(GeneralizePrecision::lessSpecific()); } @@ -1330,7 +1331,7 @@ public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCall return new ErrorType(); } - return new IntegerType(); + return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType); } public function resolveIdenticalType(Type $leftType, Type $rightType): BooleanType @@ -1469,7 +1470,7 @@ private function callOperatorTypeSpecifyingExtensions(Expr\BinaryOp $expr, Type } /** - * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div $expr + * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $expr */ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type { @@ -1536,6 +1537,9 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri $leftNumberType->isFloat()->yes() || $rightNumberType->isFloat()->yes() ) { + if ($expr instanceof Expr\BinaryOp\ShiftLeft || $expr instanceof Expr\BinaryOp\ShiftRight) { + return new IntegerType(); + } return new FloatType(); } @@ -1560,7 +1564,7 @@ private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $ri /** * @param ConstantIntegerType|IntegerRangeType $range - * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus $node + * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $node */ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): Type { @@ -1683,7 +1687,7 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T if (!is_finite($max)) { $max = null; } - } else { + } elseif ($node instanceof Expr\BinaryOp\Div) { if ($operand instanceof ConstantIntegerType) { $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null; $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null; @@ -1781,6 +1785,26 @@ private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): T return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType()); } + } elseif ($node instanceof Expr\BinaryOp\ShiftLeft) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); + } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) << $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) << $operand->getValue() : null; + } elseif ($node instanceof Expr\BinaryOp\ShiftRight) { + if (!$operand instanceof ConstantIntegerType) { + return new IntegerType(); + } + if ($operand->getValue() < 0) { + return new ErrorType(); + } + $min = $rangeMin !== null ? intval($rangeMin) >> $operand->getValue() : null; + $max = $rangeMax !== null ? intval($rangeMax) >> $operand->getValue() : null; + } else { + throw new ShouldNotHappenException(); } if (is_float($min)) { diff --git a/tests/PHPStan/Analyser/nsrt/integer-range-types.php b/tests/PHPStan/Analyser/nsrt/integer-range-types.php index 5e8706f84f..ffec07afdd 100644 --- a/tests/PHPStan/Analyser/nsrt/integer-range-types.php +++ b/tests/PHPStan/Analyser/nsrt/integer-range-types.php @@ -342,6 +342,92 @@ public function sayHello($p, $u): void assertType('float|int<-2, 2>', $p / $u); } + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftLeft($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a << 0); + assertType('int<5, max>', $b << 0); + assertType('int', $c << 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d << 0); + assertType('1|3|5', $e << 0); + assertType('*ERROR*', $f << 0); + + assertType('int<-10, 10>', $a << 1); + assertType('int<10, max>', $b << 1); + assertType('int', $c << 1); + assertType('2|50|int<10, 20>|int<60, 80>', $d << 1); + assertType('2|6|10', $e << 1); + assertType('*ERROR*', $f << 1); + + assertType('*ERROR*', $a << -1); + + assertType('int', $a << $b); + + assertType('0', null << 1); + assertType('0', false << 1); + assertType('2', true << 1); + assertType('10', "10" << 0); + assertType('*ERROR*', "ciao" << 0); + assertType('30', 15.9 << 1); + assertType('*ERROR*', array(5) << 1); + + assertType('8', 4.1 << 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float << 1.9); + } + + /** + * @param int<-5, 5> $a + * @param int<5, max> $b + * @param int $c + * @param 1|int<5, 10>|25|int<30, 40> $d + * @param 1|3.0|"5" $e + * @param 1|"ciao" $f + */ + public function shiftRight($a, $b, $c, $d, $e, $f): void + { + assertType('int<-5, 5>', $a >> 0); + assertType('int<5, max>', $b >> 0); + assertType('int', $c >> 0); + assertType('1|25|int<5, 10>|int<30, 40>', $d >> 0); + assertType('1|3|5', $e >> 0); + assertType('*ERROR*', $f >> 0); + + assertType('int<-3, 2>', $a >> 1); + assertType('int<2, max>', $b >> 1); + assertType('int', $c >> 1); + assertType('0|12|int<2, 5>|int<15, 20>', $d >> 1); + assertType('0|1|2', $e >> 1); + assertType('*ERROR*', $f >> 1); + + assertType('*ERROR*', $a >> -1); + + assertType('int', $a >> $b); + + assertType('0', null >> 1); + assertType('0', false >> 1); + assertType('0', true >> 1); + assertType('10', "10" >> 0); + assertType('*ERROR*', "ciao" >> 0); + assertType('7', 15.9 >> 1); + assertType('*ERROR*', array(5) >> 1); + + assertType('2', 4.1 >> 1.9); + + /** @var float */ + $float = 4.1; + assertType('int', $float >> 1.9); + } + /** * @param int<0, max> $positive * @param int $negative