diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 8d064c1ef3..9de365fcfa 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; @@ -9,6 +10,7 @@ use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\BooleanType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; @@ -19,8 +21,17 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function ceil; use function count; +use function floor; use function in_array; +use function is_float; +use function is_int; +use function round; +use const PHP_ROUND_HALF_DOWN; +use const PHP_ROUND_HALF_EVEN; +use const PHP_ROUND_HALF_ODD; +use const PHP_ROUND_HALF_UP; final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -56,16 +67,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $noArgsReturnType = new NullType(); } - if (count($functionCall->getArgs()) < 1) { + $args = $functionCall->getArgs(); + + if (count($args) < 1) { return $noArgsReturnType; } - $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - + $firstArgType = $scope->getType($args[0]->value); if ($firstArgType instanceof MixedType) { return $defaultReturnType; } + $functionName = $functionReflection->getName(); + $returnConstantType = $this->resolveConstantType($functionName, $args, $scope, $firstArgType); + if ($returnConstantType !== null) { + return $returnConstantType; + } + if ($this->phpVersion->hasStricterRoundFunctions()) { $allowed = TypeCombinator::union( new IntegerType(), @@ -96,4 +114,67 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new FloatType(); } + /** + * @param Arg[] $args + */ + public function resolveConstantType(string $functionName, array $args, Scope $scope, Type $argType): ?Type + { + $proc = null; + + if ($functionName === 'floor') { + $proc = static fn ($name) => floor($name); + } elseif ($functionName === 'ceil') { + $proc = static fn ($name) => ceil($name); + } elseif ($functionName === 'round') { + if (count($args) === 1) { + $proc = static fn ($name) => round($name); + } else { + if (isset($args[1]->value)) { + $precisionArg = $args[1]->value; + $precisionType = $scope->getType($precisionArg); + $precisions = $precisionType->getConstantScalarValues(); + if (count($precisions) !== 1 || !is_int($precisions[0])) { + return null; + } + $precision = $precisions[0]; + } else { + $precision = 0; + } + + if (!isset($args[2]->value)) { + $proc = static fn ($name) => round($name, $precision); + } else { + $modeArg = $args[2]->value; + $modeType = $scope->getType($modeArg); + $mode = $modeType->getConstantScalarValues(); + + if (count($mode) === 1 && in_array($mode[0], [PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD], true)) { + $proc = static fn ($name) => round($name, $precision, $mode[0]); + } + } + } + } + + if ($proc === null) { + return null; + } + + $constantScalarValues = $argType->getConstantScalarValues(); + $returnValueTypes = []; + + foreach ($constantScalarValues as $constantScalarValue) { + if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) { + $returnValueTypes = []; + break; + } + + $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); + } + + if (count($returnValueTypes) >= 1) { + return TypeCombinator::union(...$returnValueTypes); + } + + return null; + } } diff --git a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php index c618f6c8d9..05252ab6ef 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php @@ -12,8 +12,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('123.0', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('*NEVER*', round('123')); assertType('*NEVER*', round('123.456')); @@ -29,8 +29,8 @@ assertType('float', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('*NEVER*', ceil('123')); assertType('*NEVER*', ceil('123.456')); @@ -46,8 +46,8 @@ assertType('float', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('*NEVER*', floor('123')); assertType('*NEVER*', floor('123.456')); @@ -61,3 +61,87 @@ assertType('*NEVER*', floor(array(123))); assertType('*NEVER*', floor()); assertType('float', floor($_GET['foo'])); + +/** + * @param 1.11|2.22 $floatUnionA + * @param 1.5|2.5 $floatUnionB + * @param 1.1|2.2|5.5|6.6 $floatUnionC + */ +function constant(float $floatUnionA, float $floatUnionB, float $floatUnionC) +{ + assertType('3.0', round(3.4)); + assertType('4.0', round(3.5)); + assertType('4.0', round(3.6)); + assertType('4.0', round(3.6, 0)); + assertType('5.05', round(5.045, 2)); + assertType('5.06', round(5.055, 2)); + assertType('300.0', round(345, -2)); + assertType('0.0', round(345, -3)); + assertType('700.0', round(678, -2)); + assertType('1000.0', round(678, -3)); + + assertType('1.1|2.2', round($floatUnionA, 1)); + assertType('1.1|2.2', round($floatUnionA, 1, PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($floatUnionA, mode: PHP_ROUND_HALF_UP)); + assertType('2.0|3.0', round($floatUnionB, mode: PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($floatUnionB, mode: PHP_ROUND_HALF_DOWN)); + assertType('1.0|2.0|5.0|6.0', floor($floatUnionC)); + + $number = 135.79; + assertType('135.79', round($number, 3)); + assertType('135.79', round($number, 2)); + assertType('135.8', round($number, 1)); + assertType('136.0', round($number, 0)); + assertType('140.0', round($number, -1)); + assertType('100.0', round($number, -2)); + assertType('0.0', round($number, -3)); + + // Rounding modes with 9.5 + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_UP)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_ODD)); + + // Rounding modes with 8.5 + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_UP)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_ODD)); + + // Using PHP_ROUND_HALF_UP with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_UP)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_UP)); + + // Using PHP_ROUND_HALF_DOWN with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_DOWN)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_DOWN)); + + // Using PHP_ROUND_HALF_EVEN with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_EVEN)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_EVEN)); + + // Using PHP_ROUND_HALF_ODD with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_ODD)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_ODD)); +} + +/** + * @param 1.11|2.22 $floatUnion + * @param 2|3 $precisionUnion + * @param 2|4 $modeUnion + * @param 1|'2.5' $IntOrNumStr + * @param 1.11|'2.22' $floatOrNumStr + */ +function notConstant(float $floatUnion, float $precisionUnion, float $modeUnion, $IntOrNumStr, $floatOrNumStr) +{ + assertType('float', round($floatUnion, $precisionUnion, PHP_ROUND_HALF_UP)); + assertType('float', round($floatUnion, 0, $modeUnion)); + + assertType('float', round($IntOrNumStr)); + assertType('float', round($IntOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('float', round($IntOrNumStr, mode: PHP_ROUND_HALF_DOWN)); + + assertType('float', round($floatOrNumStr)); + assertType('float', round($floatOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('float', round($floatOrNumStr, mode: PHP_ROUND_HALF_DOWN)); +} diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php index 54836b7623..f128dbb6e3 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -10,8 +10,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('123.0', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('float', round('123')); assertType('float', round('123.456')); @@ -27,8 +27,8 @@ assertType('float', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('float', ceil('123')); assertType('float', ceil('123.456')); @@ -44,8 +44,8 @@ assertType('float', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('float', floor('123')); assertType('float', floor('123.456')); @@ -59,3 +59,87 @@ assertType('*NEVER*', floor(array(123))); assertType('*NEVER*', floor()); assertType('float', floor($_GET['foo'])); + +/** + * @param 1.11|2.22 $floatUnionA + * @param 1.5|2.5 $floatUnionB + * @param 1.1|2.2|5.5|6.6 $floatUnionC + */ +function constant(float $floatUnionA, float $floatUnionB, float $floatUnionC) +{ + assertType('3.0', round(3.4)); + assertType('4.0', round(3.5)); + assertType('4.0', round(3.6)); + assertType('4.0', round(3.6, 0)); + assertType('5.05', round(5.045, 2)); + assertType('5.06', round(5.055, 2)); + assertType('300.0', round(345, -2)); + assertType('0.0', round(345, -3)); + assertType('700.0', round(678, -2)); + assertType('1000.0', round(678, -3)); + + assertType('1.1|2.2', round($floatUnionA, 1)); + assertType('1.1|2.2', round($floatUnionA, 1, PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($floatUnionA, mode: PHP_ROUND_HALF_UP)); + assertType('2.0|3.0', round($floatUnionB, mode: PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($floatUnionB, mode: PHP_ROUND_HALF_DOWN)); + assertType('1.0|2.0|5.0|6.0', floor($floatUnionC)); + + $number = 135.79; + assertType('135.79', round($number, 3)); + assertType('135.79', round($number, 2)); + assertType('135.8', round($number, 1)); + assertType('136.0', round($number, 0)); + assertType('140.0', round($number, -1)); + assertType('100.0', round($number, -2)); + assertType('0.0', round($number, -3)); + + // Rounding modes with 9.5 + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_UP)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_ODD)); + + // Rounding modes with 8.5 + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_UP)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_ODD)); + + // Using PHP_ROUND_HALF_UP with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_UP)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_UP)); + + // Using PHP_ROUND_HALF_DOWN with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_DOWN)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_DOWN)); + + // Using PHP_ROUND_HALF_EVEN with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_EVEN)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_EVEN)); + + // Using PHP_ROUND_HALF_ODD with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_ODD)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_ODD)); +} + +/** + * @param 1.11|2.22 $floatUnion + * @param 2|3 $precisionUnion + * @param 2|4 $modeUnion + * @param 1|'2.5' $IntOrNumStr + * @param 1.11|'2.22' $floatOrNumStr + */ +function notConstant(float $floatUnion, float $precisionUnion, float $modeUnion, $IntOrNumStr, $floatOrNumStr) +{ + assertType('float', round($floatUnion, $precisionUnion, PHP_ROUND_HALF_UP)); + assertType('float', round($floatUnion, 0, $modeUnion)); + + assertType('float', round($IntOrNumStr)); + assertType('float', round($IntOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('float', round($IntOrNumStr, mode: PHP_ROUND_HALF_DOWN)); + + assertType('float', round($floatOrNumStr)); + assertType('float', round($floatOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('float', round($floatOrNumStr, mode: PHP_ROUND_HALF_DOWN)); +} diff --git a/tests/PHPStan/Analyser/nsrt/round.php b/tests/PHPStan/Analyser/nsrt/round.php index 3d181ca50a..0d53e32997 100644 --- a/tests/PHPStan/Analyser/nsrt/round.php +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -10,8 +10,8 @@ } // Round -assertType('float', round(123)); -assertType('float', round(123.456)); +assertType('123.0', round(123)); +assertType('123.0', round(123.456)); assertType('float', round($_GET['foo'] / 60)); assertType('float', round('123')); assertType('float', round('123.456')); @@ -27,8 +27,8 @@ assertType('(float|false)', round($_GET['foo'])); // Ceil -assertType('float', ceil(123)); -assertType('float', ceil(123.456)); +assertType('123.0', ceil(123)); +assertType('124.0', ceil(123.456)); assertType('float', ceil($_GET['foo'] / 60)); assertType('float', ceil('123')); assertType('float', ceil('123.456')); @@ -44,8 +44,8 @@ assertType('(float|false)', ceil($_GET['foo'])); // Floor -assertType('float', floor(123)); -assertType('float', floor(123.456)); +assertType('123.0', floor(123)); +assertType('123.0', floor(123.456)); assertType('float', floor($_GET['foo'] / 60)); assertType('float', floor('123')); assertType('float', floor('123.456')); @@ -59,3 +59,79 @@ assertType('false', floor(array(123))); assertType('null', floor()); assertType('(float|false)', floor($_GET['foo'])); + +/** + * @param 1.11|2.22 $floatUnionA + * @param 1.5|2.5 $floatUnionB + * @param 1.1|2.2|5.5|6.6 $floatUnionC + */ +function constant(float $floatUnionA, float $floatUnionB, float $floatUnionC) +{ + assertType('3.0', round(3.4)); + assertType('4.0', round(3.5)); + assertType('4.0', round(3.6)); + assertType('4.0', round(3.6, 0)); + assertType('5.05', round(5.045, 2)); + assertType('5.06', round(5.055, 2)); + assertType('300.0', round(345, -2)); + assertType('0.0', round(345, -3)); + assertType('700.0', round(678, -2)); + assertType('1000.0', round(678, -3)); + + assertType('1.1|2.2', round($floatUnionA, 1)); + assertType('1.1|2.2', round($floatUnionA, 1, PHP_ROUND_HALF_UP)); + assertType('1.0|2.0|5.0|6.0', floor($floatUnionC)); + + $number = 135.79; + assertType('135.79', round($number, 3)); + assertType('135.79', round($number, 2)); + assertType('135.8', round($number, 1)); + assertType('136.0', round($number, 0)); + assertType('140.0', round($number, -1)); + assertType('100.0', round($number, -2)); + assertType('0.0', round($number, -3)); + + // Rounding modes with 9.5 + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_UP)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('10.0', round(9.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(9.5, 0, PHP_ROUND_HALF_ODD)); + + // Rounding modes with 8.5 + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_UP)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_DOWN)); + assertType('8.0', round(8.5, 0, PHP_ROUND_HALF_EVEN)); + assertType('9.0', round(8.5, 0, PHP_ROUND_HALF_ODD)); + + // Using PHP_ROUND_HALF_UP with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_UP)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_UP)); + + // Using PHP_ROUND_HALF_DOWN with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_DOWN)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_DOWN)); + + // Using PHP_ROUND_HALF_EVEN with 1 decimal digit precision + assertType('1.6', round( 1.55, 1, PHP_ROUND_HALF_EVEN)); + assertType('-1.6', round(-1.55, 1, PHP_ROUND_HALF_EVEN)); + + // Using PHP_ROUND_HALF_ODD with 1 decimal digit precision + assertType('1.5', round( 1.55, 1, PHP_ROUND_HALF_ODD)); + assertType('-1.5', round(-1.55, 1, PHP_ROUND_HALF_ODD)); +} + +/** + * @param 1.11|2.22 $floatUnion + * @param 2|3 $precisionUnion + * @param 2|4 $modeUnion + * @param 1|'2.5' $IntOrNumStr + * @param 1.11|'2.22' $floatOrNumStr + */ +function notConstant(float $floatUnion, float $precisionUnion, float $modeUnion, $IntOrNumStr, $floatOrNumStr) +{ + assertType('float', round($floatUnion, $precisionUnion, PHP_ROUND_HALF_UP)); + assertType('float', round($floatUnion, 0, $modeUnion)); + + assertType('float', round($IntOrNumStr)); + assertType('float', round($floatOrNumStr)); +}