From 2acc062dfe2181d22ab518105f4ec7a214e94e22 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:10:15 +0900 Subject: [PATCH 01/26] RoundFunctionReturnTypeExtension supports types with ConstantScalar --- .../Php/RoundFunctionReturnTypeExtension.php | 96 ++++++++++++++++++- tests/PHPStan/Analyser/nsrt/round-php8.php | 39 ++++++++ 2 files changed, 134 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 2bc17560339..da5d70619b4 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -10,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,9 +20,13 @@ use PHPStan\Type\NullType; use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\UnionType; use function count; use function in_array; +use function is_int; +use function is_float; #[AutowiredService] final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -58,12 +63,48 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $noArgsReturnType = new NullType(); } - if (count($functionCall->getArgs()) < 1) { + $args = $functionCall->getArgs(); + + //引数長さ0ならNeverType/NullTypeを返す + if (count($args) < 1) { return $noArgsReturnType; } + + $argType = $scope->getType($args[0]->value); + + $functionName = $functionReflection->getName(); + + $proc = $this->getProc($functionName, $args, $scope); + + if ($proc !== null) { + $constantScalarValues = $argType->getConstantScalarValues(); + $rv = array(); + + foreach ($constantScalarValues as $constantScalarValue) { + + if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) { + $rv = []; + break; + } + + $value = $proc($constantScalarValue); + + $rv[] = new ConstantFloatType($value); + } + + if (count($rv) > 1) { + + $rvUnion = TypeCombinator::union(...array_map(static fn ($l) => $l, $rv)); + return $rvUnion; + } + } + + + //最初の引数のTypeを取得 $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + //$firstArgType が MixedTypeなら $defaultReturnTypeを返す if ($firstArgType instanceof MixedType) { return $defaultReturnType; } @@ -87,16 +128,69 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); } + + + //スーパータイプではないなら、NeverTypeを返す if ($allowed->isSuperTypeOf($firstArgType)->no()) { // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); } + + } elseif ($firstArgType->isArray()->yes()) { // PHP 7 returns false if the parameter is an array. + // パラメータが配列の場合は false を返します。 return new ConstantBooleanType(false); } return new FloatType(); } + /** + * @param string $functionName + * @param array $args + * @param Scope $scope + * @return \Closure + */ + public function getProc(string $functionName, array $args, Scope $scope): ?\Closure + { + if ($functionName == "floor") { + return fn($name) => floor($name); + } + if ($functionName == "ceil") { + return fn($name) => ceil($name); + } + if ($functionName === 'round') { + if (count($args) == 1) { + return fn($name) => round($name); + } + if (isset($args[1]->value)) { + $precisionArg = $args[1]->value; + $precisionType = $scope->getType($precisionArg); + $precisions = $precisionType->getConstantScalarValues(); + if (count($precisions) == 1) { + $precision = $precisions[0]; + } + else{ + return null; + } + } else { + $precision = 0; + } + + if (isset($args[2]->value)) { + $modeArg = $args[2]->value; + $modeType = $scope->getType($modeArg); + $mode = $modeType->getConstantScalarValues(); + + if (count($mode) == 1) { + return fn($name) => round($name, $precision, $mode[0]); + } + } + else{ + return fn($name) => round($name, $precision); + } + } + return null; + } } diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php index 54836b7623c..86fd4ee8e47 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -59,3 +59,42 @@ assertType('*NEVER*', floor(array(123))); assertType('*NEVER*', floor()); assertType('float', floor($_GET['foo'])); + +/** + * @param 1.1|2.2|5.5|6.6 $n + */ +function f(float $n): void +{ + assertType('1.0|2.0|5.0|6.0', floor($n)); +} + + +/** + * @param 1.11|2.22 $n + * @param 2|3 $m + * @param 2|4 $p + * @param 1.5|2.5 $q + */ +function g(float $n,float $m ,float $p , float $q): void +{ + assertType('1.1|2.2', round($n,1)); + assertType('1.1|2.2', round($n,1,PHP_ROUND_HALF_UP)); + assertType('float', round($n,$m,PHP_ROUND_HALF_UP)); + assertType('float', round($n,0,$p)); + assertType('1.1|2.2', round($n,1)); + assertType('1.0|2.0', round($n,mode:PHP_ROUND_HALF_UP)); + assertType('2.0|3.0', round($q,mode:PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($q,mode:PHP_ROUND_HALF_DOWN)); + +// assertType(3,round(3.4)); +// assertType(4,round(3.5)); +// assertType(4,round(3.6)); +// assertType(4.round(3.6, 0)); +// assertType(5.05.round(5.045, 2)); +// assertType(5.06round(5.055, 2)); +// assertType(round(300,345, -2)); +// assertType(round(0,345, -3)); +// assertType(round(700,678, -2)); +// assertType(round(1000,678, -3)); +// assertType('float', round($n,2,3)); +} From 620b88478d899060432dd31a8ad1ef1db336bf6b Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:51:12 +0900 Subject: [PATCH 02/26] fix RoundFunctionReturnTypeExtension --- .../Php/RoundFunctionReturnTypeExtension.php | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index da5d70619b4..af7149208a9 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -2,6 +2,7 @@ namespace PHPStan\Type\Php; +use Closure; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; @@ -21,12 +22,18 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\UnionType; +use function array_map; +use function ceil; use function count; +use function floor; use function in_array; +<<<<<<< HEAD use function is_int; +======= +>>>>>>> 1c0c0cb16 (fix RoundFunctionReturnTypeExtension) use function is_float; +use function is_int; +use function round; #[AutowiredService] final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -70,7 +77,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $noArgsReturnType; } - $argType = $scope->getType($args[0]->value); $functionName = $functionReflection->getName(); @@ -79,7 +85,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($proc !== null) { $constantScalarValues = $argType->getConstantScalarValues(); - $rv = array(); + $rv = []; foreach ($constantScalarValues as $constantScalarValue) { @@ -93,14 +99,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $rv[] = new ConstantFloatType($value); } - if (count($rv) > 1) { + if (count($rv) >= 1) { $rvUnion = TypeCombinator::union(...array_map(static fn ($l) => $l, $rv)); return $rvUnion; } } - //最初の引数のTypeを取得 $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); @@ -128,15 +133,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); } - - //スーパータイプではないなら、NeverTypeを返す if ($allowed->isSuperTypeOf($firstArgType)->no()) { // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); } - } elseif ($firstArgType->isArray()->yes()) { // PHP 7 returns false if the parameter is an array. // パラメータが配列の場合は false を返します。 @@ -147,50 +149,46 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } /** - * @param string $functionName * @param array $args - * @param Scope $scope - * @return \Closure */ - public function getProc(string $functionName, array $args, Scope $scope): ?\Closure + public function getProc(string $functionName, array $args, Scope $scope): ?Closure { - if ($functionName == "floor") { - return fn($name) => floor($name); + if ($functionName === 'floor') { + return static fn ($name) => floor($name); } - if ($functionName == "ceil") { - return fn($name) => ceil($name); + if ($functionName === 'ceil') { + return static fn ($name) => ceil($name); } if ($functionName === 'round') { - if (count($args) == 1) { - return fn($name) => round($name); + if (count($args) === 1) { + return static fn ($name) => round($name); } if (isset($args[1]->value)) { $precisionArg = $args[1]->value; $precisionType = $scope->getType($precisionArg); $precisions = $precisionType->getConstantScalarValues(); - if (count($precisions) == 1) { - $precision = $precisions[0]; - } - else{ + if (count($precisions) !== 1) { return null; } + + $precision = $precisions[0]; } else { $precision = 0; } - if (isset($args[2]->value)) { - $modeArg = $args[2]->value; - $modeType = $scope->getType($modeArg); - $mode = $modeType->getConstantScalarValues(); - - if (count($mode) == 1) { - return fn($name) => round($name, $precision, $mode[0]); - } + if (!isset($args[2]->value)) { + return static fn ($name) => round($name, $precision); } - else{ - return fn($name) => round($name, $precision); + + $modeArg = $args[2]->value; + $modeType = $scope->getType($modeArg); + $mode = $modeType->getConstantScalarValues(); + + if (count($mode) === 1) { + return static fn ($name) => round($name, $precision, $mode[0]); } } return null; } + } From c9dc80f4addb906eb45ca16b7b70a0a0866159cd Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:53:29 +0900 Subject: [PATCH 03/26] add tests --- tests/PHPStan/Analyser/nsrt/round-php8.php | 70 ++++++++++++++++------ 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php index 86fd4ee8e47..3d841d92381 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')); @@ -86,15 +86,51 @@ function g(float $n,float $m ,float $p , float $q): void assertType('2.0|3.0', round($q,mode:PHP_ROUND_HALF_UP)); assertType('1.0|2.0', round($q,mode:PHP_ROUND_HALF_DOWN)); -// assertType(3,round(3.4)); -// assertType(4,round(3.5)); -// assertType(4,round(3.6)); -// assertType(4.round(3.6, 0)); -// assertType(5.05.round(5.045, 2)); -// assertType(5.06round(5.055, 2)); -// assertType(round(300,345, -2)); -// assertType(round(0,345, -3)); -// assertType(round(700,678, -2)); -// assertType(round(1000,678, -3)); -// assertType('float', round($n,2,3)); + 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)); + + $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)); } From 13eb139f5c589640ee96b6747cae30cb1624318e Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 11:04:19 +0900 Subject: [PATCH 04/26] fix style --- .../Php/RoundFunctionReturnTypeExtension.php | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index af7149208a9..c712444cb61 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Php; use Closure; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\DependencyInjection\AutowiredService; @@ -27,10 +28,6 @@ use function count; use function floor; use function in_array; -<<<<<<< HEAD -use function is_int; -======= ->>>>>>> 1c0c0cb16 (fix RoundFunctionReturnTypeExtension) use function is_float; use function is_int; use function round; @@ -72,49 +69,47 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $args = $functionCall->getArgs(); - //引数長さ0ならNeverType/NullTypeを返す if (count($args) < 1) { return $noArgsReturnType; } $argType = $scope->getType($args[0]->value); - $functionName = $functionReflection->getName(); - $proc = $this->getProc($functionName, $args, $scope); if ($proc !== null) { $constantScalarValues = $argType->getConstantScalarValues(); - $rv = []; + $returnValueTypes = []; foreach ($constantScalarValues as $constantScalarValue) { - if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) { - $rv = []; + $returnValueTypes = []; break; } - $value = $proc($constantScalarValue); - - $rv[] = new ConstantFloatType($value); + $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); } - if (count($rv) >= 1) { - - $rvUnion = TypeCombinator::union(...array_map(static fn ($l) => $l, $rv)); - return $rvUnion; + if (count($returnValueTypes) >= 1) { + return TypeCombinator::union(...array_map(static fn ($l) => $l, $returnValueTypes)); } } - //最初の引数のTypeを取得 $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - //$firstArgType が MixedTypeなら $defaultReturnTypeを返す if ($firstArgType instanceof MixedType) { return $defaultReturnType; } if ($this->phpVersion->hasStricterRoundFunctions()) { +<<<<<<< HEAD +======= + $allowed = TypeCombinator::union( + new IntegerType(), + new FloatType(), + ); + +>>>>>>> 05dc61fe1 (fix style) if (!$scope->isDeclareStrictTypes()) { $allowed = new UnionType([ new IntegerType(), @@ -133,7 +128,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, ]); } - //スーパータイプではないなら、NeverTypeを返す if ($allowed->isSuperTypeOf($firstArgType)->no()) { // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); @@ -141,7 +135,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } elseif ($firstArgType->isArray()->yes()) { // PHP 7 returns false if the parameter is an array. - // パラメータが配列の場合は false を返します。 return new ConstantBooleanType(false); } @@ -149,16 +142,18 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } /** - * @param array $args + * @param Arg[] $args */ public function getProc(string $functionName, array $args, Scope $scope): ?Closure { if ($functionName === 'floor') { return static fn ($name) => floor($name); } + if ($functionName === 'ceil') { return static fn ($name) => ceil($name); } + if ($functionName === 'round') { if (count($args) === 1) { return static fn ($name) => round($name); @@ -188,6 +183,7 @@ public function getProc(string $functionName, array $args, Scope $scope): ?Closu return static fn ($name) => round($name, $precision, $mode[0]); } } + return null; } From 89dc32e23ee14d2a1a2971d66cc52936ec0e162e Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 12:15:04 +0900 Subject: [PATCH 05/26] add tests --- .../Analyser/nsrt/round-php8-strict-types.php | 96 +++++++++++++++++-- tests/PHPStan/Analyser/nsrt/round-php8.php | 55 ++++++----- tests/PHPStan/Analyser/nsrt/round.php | 88 +++++++++++++++-- 3 files changed, 204 insertions(+), 35 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php b/tests/PHPStan/Analyser/nsrt/round-php8-strict-types.php index c618f6c8d9a..05252ab6efd 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 3d841d92381..f128dbb6e31 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -61,31 +61,12 @@ assertType('float', floor($_GET['foo'])); /** - * @param 1.1|2.2|5.5|6.6 $n + * @param 1.11|2.22 $floatUnionA + * @param 1.5|2.5 $floatUnionB + * @param 1.1|2.2|5.5|6.6 $floatUnionC */ -function f(float $n): void +function constant(float $floatUnionA, float $floatUnionB, float $floatUnionC) { - assertType('1.0|2.0|5.0|6.0', floor($n)); -} - - -/** - * @param 1.11|2.22 $n - * @param 2|3 $m - * @param 2|4 $p - * @param 1.5|2.5 $q - */ -function g(float $n,float $m ,float $p , float $q): void -{ - assertType('1.1|2.2', round($n,1)); - assertType('1.1|2.2', round($n,1,PHP_ROUND_HALF_UP)); - assertType('float', round($n,$m,PHP_ROUND_HALF_UP)); - assertType('float', round($n,0,$p)); - assertType('1.1|2.2', round($n,1)); - assertType('1.0|2.0', round($n,mode:PHP_ROUND_HALF_UP)); - assertType('2.0|3.0', round($q,mode:PHP_ROUND_HALF_UP)); - assertType('1.0|2.0', round($q,mode:PHP_ROUND_HALF_DOWN)); - assertType('3.0', round(3.4)); assertType('4.0', round(3.5)); assertType('4.0', round(3.6)); @@ -97,6 +78,13 @@ function g(float $n,float $m ,float $p , float $q): void 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)); @@ -134,3 +122,24 @@ function g(float $n,float $m ,float $p , float $q): void 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 3d181ca50a6..0d53e329976 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)); +} From e6c7c061ade020fc021b945535259e74b403325c Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 13:45:29 +0900 Subject: [PATCH 06/26] fix --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index c712444cb61..647e9e485f7 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -162,10 +162,9 @@ public function getProc(string $functionName, array $args, Scope $scope): ?Closu $precisionArg = $args[1]->value; $precisionType = $scope->getType($precisionArg); $precisions = $precisionType->getConstantScalarValues(); - if (count($precisions) !== 1) { + if (count($precisions) !== 1 || !is_int($precisions[0])) { return null; } - $precision = $precisions[0]; } else { $precision = 0; @@ -179,7 +178,7 @@ public function getProc(string $functionName, array $args, Scope $scope): ?Closu $modeType = $scope->getType($modeArg); $mode = $modeType->getConstantScalarValues(); - if (count($mode) === 1) { + if (count($mode) === 1 && is_int($mode[0])) { return static fn ($name) => round($name, $precision, $mode[0]); } } From 83af3d53ceb57b40465ca76c671782ea7cdc1c7a Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 14:01:39 +0900 Subject: [PATCH 07/26] refactor method --- .../Php/RoundFunctionReturnTypeExtension.php | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 647e9e485f7..7f813d4bb46 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Php; -use Closure; use PhpParser\Node\Arg; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; @@ -73,34 +72,17 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $noArgsReturnType; } - $argType = $scope->getType($args[0]->value); - $functionName = $functionReflection->getName(); - $proc = $this->getProc($functionName, $args, $scope); - - if ($proc !== 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(...array_map(static fn ($l) => $l, $returnValueTypes)); - } - } - - $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()) { <<<<<<< HEAD ======= @@ -144,46 +126,64 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, /** * @param Arg[] $args */ - public function getProc(string $functionName, array $args, Scope $scope): ?Closure + public function resolveConstantType(string $functionName, array $args, Scope $scope, Type $argType): ?Type { + $proc = null; + if ($functionName === 'floor') { - return static fn ($name) => floor($name); - } + $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 ($functionName === 'ceil') { - return static fn ($name) => ceil($name); - } + 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 ($functionName === 'round') { - if (count($args) === 1) { - return static fn ($name) => round($name); - } - 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; + if (count($mode) === 1 && is_int($mode[0])) { + $proc = static fn ($name) => round($name, $precision, $mode[0]); + } } - $precision = $precisions[0]; - } else { - $precision = 0; } + } - if (!isset($args[2]->value)) { - return static fn ($name) => round($name, $precision); - } + if ($proc === null) { + return null; + } - $modeArg = $args[2]->value; - $modeType = $scope->getType($modeArg); - $mode = $modeType->getConstantScalarValues(); + $constantScalarValues = $argType->getConstantScalarValues(); + $returnValueTypes = []; - if (count($mode) === 1 && is_int($mode[0])) { - return static fn ($name) => round($name, $precision, $mode[0]); + 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(...array_map(static fn ($l) => $l, $returnValueTypes)); } return null; } - } From 9ab554758781eafd98b5b82a798f662c99df0ca3 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 14:13:23 +0900 Subject: [PATCH 08/26] fix --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 7f813d4bb46..3c3bdaf54c4 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -30,6 +30,10 @@ 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; #[AutowiredService] final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -157,7 +161,7 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc $modeType = $scope->getType($modeArg); $mode = $modeType->getConstantScalarValues(); - if (count($mode) === 1 && is_int($mode[0])) { + 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]); } } From 77a4c8568ad2a0cabeeb1ca3ce806b2e2612cd21 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 18:44:30 +0900 Subject: [PATCH 09/26] fix --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 3c3bdaf54c4..0c3bd987986 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -22,7 +22,7 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function array_map; +use PHPStan\Type\UnionType; use function ceil; use function count; use function floor; @@ -88,14 +88,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if ($this->phpVersion->hasStricterRoundFunctions()) { -<<<<<<< HEAD -======= - $allowed = TypeCombinator::union( - new IntegerType(), - new FloatType(), - ); - ->>>>>>> 05dc61fe1 (fix style) if (!$scope->isDeclareStrictTypes()) { $allowed = new UnionType([ new IntegerType(), @@ -118,7 +110,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); } - } elseif ($firstArgType->isArray()->yes()) { // PHP 7 returns false if the parameter is an array. return new ConstantBooleanType(false); @@ -185,9 +176,10 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc } if (count($returnValueTypes) >= 1) { - return TypeCombinator::union(...array_map(static fn ($l) => $l, $returnValueTypes)); + return TypeCombinator::union(...$returnValueTypes); } return null; } + } From cb5858594a6c9965e0c08f7823e807777f833e44 Mon Sep 17 00:00:00 2001 From: maruyama Date: Sun, 20 Sep 2026 19:51:47 +0900 Subject: [PATCH 10/26] fix : Return null directly for unsupported constant values --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 0c3bd987986..dcae51e7242 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -168,8 +168,7 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc foreach ($constantScalarValues as $constantScalarValue) { if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) { - $returnValueTypes = []; - break; + return null; } $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); From 283b980c12c56e87af031c7efeb6d47335aab848 Mon Sep 17 00:00:00 2001 From: maruyama Date: Sun, 20 Sep 2026 22:29:39 +0900 Subject: [PATCH 11/26] feat : Support RoundingMode in round constant evaluation --- .../Php/RoundFunctionReturnTypeExtension.php | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index dcae51e7242..865b143eb66 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -146,14 +146,36 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc } if (!isset($args[2]->value)) { - $proc = static fn ($name) => round($name, $precision); + $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]); + $proc = static fn($name) => round($name, $precision, $mode[0]); + } else { + $enumCase = $modeType->getEnumCaseObject(); + + if ($enumCase === null || $enumCase->getClassName() !== 'RoundingMode') { + return null; + } + + $mode = match ($enumCase->getEnumCaseName()) { + 'HalfAwayFromZero' => PHP_ROUND_HALF_UP, + 'HalfTowardsZero' => PHP_ROUND_HALF_DOWN, + 'HalfEven' => PHP_ROUND_HALF_EVEN, + 'HalfOdd' => PHP_ROUND_HALF_ODD, + default => null, + }; + + if ($mode === null) { + return null; + } + + $proc = static fn($name) => round($name, $precision, $mode); } } } From 83b0304db935e92191112a56640d476785247dcb Mon Sep 17 00:00:00 2001 From: maruyama Date: Sun, 20 Sep 2026 22:37:10 +0900 Subject: [PATCH 12/26] test : Add RoundingMode constant evaluation tests --- tests/PHPStan/Analyser/nsrt/round-php84.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/round-php84.php diff --git a/tests/PHPStan/Analyser/nsrt/round-php84.php b/tests/PHPStan/Analyser/nsrt/round-php84.php new file mode 100644 index 00000000000..06fc67c9d89 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/round-php84.php @@ -0,0 +1,18 @@ += 8.4 + +namespace RoundFamilyTestPHP84; + +use function PHPStan\Testing\assertType; + +function constantRoundingMode(): void +{ + assertType('10.0', round(9.5, 0, \RoundingMode::HalfAwayFromZero)); + assertType('9.0', round(9.5, 0, \RoundingMode::HalfTowardsZero)); + assertType('10.0', round(9.5, 0, \RoundingMode::HalfEven)); + assertType('9.0', round(9.5, 0, \RoundingMode::HalfOdd)); + + assertType('float', round(9.5, 0, \RoundingMode::TowardsZero)); + assertType('float', round(9.5, 0, \RoundingMode::AwayFromZero)); + assertType('float', round(9.5, 0, \RoundingMode::NegativeInfinity)); + assertType('float', round(9.5, 0, \RoundingMode::PositiveInfinity)); +} \ No newline at end of file From 7d93f5dcf0f89b0f03f42799da5f64ef819278f2 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:18:46 +0900 Subject: [PATCH 13/26] test : Test named RoundingMode argument --- tests/PHPStan/Analyser/nsrt/round-php84.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/PHPStan/Analyser/nsrt/round-php84.php b/tests/PHPStan/Analyser/nsrt/round-php84.php index 06fc67c9d89..d928feb5332 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php84.php +++ b/tests/PHPStan/Analyser/nsrt/round-php84.php @@ -15,4 +15,6 @@ function constantRoundingMode(): void assertType('float', round(9.5, 0, \RoundingMode::AwayFromZero)); assertType('float', round(9.5, 0, \RoundingMode::NegativeInfinity)); assertType('float', round(9.5, 0, \RoundingMode::PositiveInfinity)); -} \ No newline at end of file + + assertType('10.0', round(9.5, mode: \RoundingMode::HalfAwayFromZero)); +} From aea5c96aecef32763a31e89e1168472a23d17c42 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:19:23 +0900 Subject: [PATCH 14/26] test : Test RoundingMode union fallback --- tests/PHPStan/Analyser/nsrt/round-php84.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/round-php84.php b/tests/PHPStan/Analyser/nsrt/round-php84.php index d928feb5332..b45eb1f3b02 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php84.php +++ b/tests/PHPStan/Analyser/nsrt/round-php84.php @@ -18,3 +18,12 @@ function constantRoundingMode(): void assertType('10.0', round(9.5, mode: \RoundingMode::HalfAwayFromZero)); } + + +/** + * @param \RoundingMode::HalfEven|\RoundingMode::HalfOdd $mode + */ +function nonConstantRoundingMode(\RoundingMode $mode): void +{ + assertType('float', round(9.5, 0, $mode)); +} \ No newline at end of file From 030b87c5286718e595b93318de723a4001608508 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:22:57 +0900 Subject: [PATCH 15/26] refactor : round mode resolution --- .../Php/RoundFunctionReturnTypeExtension.php | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 865b143eb66..9a2a0920906 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -118,6 +118,34 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new FloatType(); } + private function resolveRoundMode(Type $modeType): ?int + { + $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)) { + return $mode[0]; + } + + $enumCase = $modeType->getEnumCaseObject(); + + if ($enumCase === null || $enumCase->getClassName() !== 'RoundingMode') { + return null; + } + + return match ($enumCase->getEnumCaseName()) { + 'HalfAwayFromZero' => PHP_ROUND_HALF_UP, + 'HalfTowardsZero' => PHP_ROUND_HALF_DOWN, + 'HalfEven' => PHP_ROUND_HALF_EVEN, + 'HalfOdd' => PHP_ROUND_HALF_ODD, + default => null, + }; + } + /** * @param Arg[] $args */ @@ -145,39 +173,18 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc $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]); - } else { - $enumCase = $modeType->getEnumCaseObject(); - - if ($enumCase === null || $enumCase->getClassName() !== 'RoundingMode') { - return null; - } - - $mode = match ($enumCase->getEnumCaseName()) { - 'HalfAwayFromZero' => PHP_ROUND_HALF_UP, - 'HalfTowardsZero' => PHP_ROUND_HALF_DOWN, - 'HalfEven' => PHP_ROUND_HALF_EVEN, - 'HalfOdd' => PHP_ROUND_HALF_ODD, - default => null, - }; +if (!isset($args[2]->value)) { + $proc = static fn ($name) => round($name, $precision); +} else { + $modeType = $scope->getType($args[2]->value); + $mode = $this->resolveRoundMode($modeType); - if ($mode === null) { - return null; - } + if ($mode === null) { + return null; + } - $proc = static fn($name) => round($name, $precision, $mode); - } - } + $proc = static fn ($name) => round($name, $precision, $mode); +} } } From 14fc60294a700feaaf7297b1654df9df201090bf Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:28:33 +0900 Subject: [PATCH 16/26] fix : coding style --- .../Php/RoundFunctionReturnTypeExtension.php | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 9a2a0920906..4fe7718d879 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -29,6 +29,8 @@ use function in_array; use function is_float; use function is_int; +use function is_numeric; +use function is_string; use function round; use const PHP_ROUND_HALF_DOWN; use const PHP_ROUND_HALF_EVEN; @@ -173,18 +175,18 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc $precision = 0; } -if (!isset($args[2]->value)) { - $proc = static fn ($name) => round($name, $precision); -} else { - $modeType = $scope->getType($args[2]->value); - $mode = $this->resolveRoundMode($modeType); + if (!isset($args[2]->value)) { + $proc = static fn ($name) => round($name, $precision); + } else { + $modeType = $scope->getType($args[2]->value); + $mode = $this->resolveRoundMode($modeType); - if ($mode === null) { - return null; - } + if ($mode === null) { + return null; + } - $proc = static fn ($name) => round($name, $precision, $mode); -} + $proc = static fn ($name) => round($name, $precision, $mode); + } } } From b8f635e2c6b29308ca15ee6a1c7db69ac0ca0d19 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:29:06 +0900 Subject: [PATCH 17/26] feat : support constant numeric strings in non-strict mode --- .../Php/RoundFunctionReturnTypeExtension.php | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 4fe7718d879..778cc4a6713 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -198,11 +198,21 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc $returnValueTypes = []; foreach ($constantScalarValues as $constantScalarValue) { - if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) { - return null; + if (is_int($constantScalarValue) || is_float($constantScalarValue)) { + $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); + continue; } - $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); + if ( + !$scope->isDeclareStrictTypes() + && is_string($constantScalarValue) + && is_numeric($constantScalarValue) + ) { + $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); + continue; + } + + return null; } if (count($returnValueTypes) >= 1) { From 8defbf0de1c4fab054a06eb7d260b6a07365d7fd Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:35:07 +0900 Subject: [PATCH 18/26] fix : Cast numeric strings before constant evaluation --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 778cc4a6713..395d0503541 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -208,7 +208,7 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc && is_string($constantScalarValue) && is_numeric($constantScalarValue) ) { - $returnValueTypes[] = new ConstantFloatType($proc($constantScalarValue)); + $returnValueTypes[] = new ConstantFloatType($proc((float) $constantScalarValue)); continue; } From 948fa395f71ffeafec409b0b920cca6a7004a5f2 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:35:28 +0900 Subject: [PATCH 19/26] test : numeric string constant evaluation --- tests/PHPStan/Analyser/nsrt/round-php8.php | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/round-php8.php b/tests/PHPStan/Analyser/nsrt/round-php8.php index f128dbb6e31..ad1a162b05c 100644 --- a/tests/PHPStan/Analyser/nsrt/round-php8.php +++ b/tests/PHPStan/Analyser/nsrt/round-php8.php @@ -13,8 +13,8 @@ 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')); +assertType('123.0', round('123')); +assertType('123.0', round('123.456')); assertType('float', round(null)); assertType('float', round($maybeNull)); assertType('float', round(true)); @@ -30,8 +30,8 @@ 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')); +assertType('123.0', ceil('123')); +assertType('124.0', ceil('123.456')); assertType('float', ceil(null)); assertType('float', ceil($maybeNull)); assertType('float', ceil(true)); @@ -47,8 +47,8 @@ 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')); +assertType('123.0', floor('123')); +assertType('123.0', floor('123.456')); assertType('float', floor(null)); assertType('float', floor($maybeNull)); assertType('float', floor(true)); @@ -135,11 +135,11 @@ function notConstant(float $floatUnion, float $precisionUnion, float $modeUnion, 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('1.0|3.0', round($IntOrNumStr)); + assertType('1.0|3.0', round($IntOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', 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)); + assertType('1.0|2.0', round($floatOrNumStr)); + assertType('1.0|2.0', round($floatOrNumStr, mode: PHP_ROUND_HALF_UP)); + assertType('1.0|2.0', round($floatOrNumStr, mode: PHP_ROUND_HALF_DOWN)); } From 34344eb3f2a5c7a4b337af10d80525bd099734ef Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:50:54 +0900 Subject: [PATCH 20/26] Fix coding style --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 395d0503541..4c8c1fa16e1 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -221,5 +221,5 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc return null; } - + } From c371271979782664b4b51f5314de90cb454fa4e1 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:55:06 +0900 Subject: [PATCH 21/26] fix : round mode return type --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 4c8c1fa16e1..31e6a258534 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -119,8 +119,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new FloatType(); } - + /** + * @return 1|2|3|4|null + */ private function resolveRoundMode(Type $modeType): ?int + + { $mode = $modeType->getConstantScalarValues(); @@ -221,5 +225,5 @@ public function resolveConstantType(string $functionName, array $args, Scope $sc return null; } - + } From 5b3ac6dc8b61787da7e1140805d560ad9df23365 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 00:59:09 +0900 Subject: [PATCH 22/26] fix : narrow round mode return type --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 31e6a258534..801f5208efb 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -121,10 +121,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } /** * @return 1|2|3|4|null + * // Fall back to the default return type for RoundingMode cases + * // without a corresponding PHP_ROUND_HALF_* constant. */ private function resolveRoundMode(Type $modeType): ?int - - { $mode = $modeType->getConstantScalarValues(); From 6cce71bdbca01b834670bc7ac0ac134c329d52a7 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 01:03:32 +0900 Subject: [PATCH 23/26] fix : lint --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 801f5208efb..6013be3a0d4 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -119,11 +119,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new FloatType(); } + /** * @return 1|2|3|4|null * // Fall back to the default return type for RoundingMode cases * // without a corresponding PHP_ROUND_HALF_* constant. - */ + */ private function resolveRoundMode(Type $modeType): ?int { $mode = $modeType->getConstantScalarValues(); From 54255ca89b734a3d50250d0a872aac670156c5b3 Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 01:03:50 +0900 Subject: [PATCH 24/26] fix : keep round mode resolution compatible with PHP 7.4 --- .../Php/RoundFunctionReturnTypeExtension.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 6013be3a0d4..1773d89964b 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -144,13 +144,18 @@ private function resolveRoundMode(Type $modeType): ?int return null; } - return match ($enumCase->getEnumCaseName()) { - 'HalfAwayFromZero' => PHP_ROUND_HALF_UP, - 'HalfTowardsZero' => PHP_ROUND_HALF_DOWN, - 'HalfEven' => PHP_ROUND_HALF_EVEN, - 'HalfOdd' => PHP_ROUND_HALF_ODD, - default => null, - }; + switch ($enumCase->getEnumCaseName()) { + case 'HalfAwayFromZero': + return PHP_ROUND_HALF_UP; + case 'HalfTowardsZero': + return PHP_ROUND_HALF_DOWN; + case 'HalfEven': + return PHP_ROUND_HALF_EVEN; + case 'HalfOdd': + return PHP_ROUND_HALF_ODD; + default: + return null; + } } /** From c83de21059a43f30bf85cd366d49bf95f091b63e Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 01:13:10 +0900 Subject: [PATCH 25/26] test : update PHP 7 numeric string tests --- tests/PHPStan/Analyser/nsrt/round.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/round.php b/tests/PHPStan/Analyser/nsrt/round.php index 0d53e329976..6a28f4ad045 100644 --- a/tests/PHPStan/Analyser/nsrt/round.php +++ b/tests/PHPStan/Analyser/nsrt/round.php @@ -13,8 +13,8 @@ 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')); +assertType('123.0', round('123')); +assertType('123.0', round('123.456')); assertType('float', round(null)); assertType('float', round($maybeNull)); assertType('float', round(true)); @@ -30,8 +30,8 @@ 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')); +assertType('123.0', ceil('123')); +assertType('124.0', ceil('123.456')); assertType('float', ceil(null)); assertType('float', ceil($maybeNull)); assertType('float', ceil(true)); @@ -47,8 +47,8 @@ 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')); +assertType('123.0', floor('123')); +assertType('123.0', floor('123.456')); assertType('float', floor(null)); assertType('float', floor($maybeNull)); assertType('float', floor(true)); @@ -132,6 +132,6 @@ function notConstant(float $floatUnion, float $precisionUnion, float $modeUnion, assertType('float', round($floatUnion, $precisionUnion, PHP_ROUND_HALF_UP)); assertType('float', round($floatUnion, 0, $modeUnion)); - assertType('float', round($IntOrNumStr)); - assertType('float', round($floatOrNumStr)); + assertType('1.0|3.0', round($IntOrNumStr)); + assertType('1.0|2.0', round($floatOrNumStr)); } From 4b4b1dca7095415e81cf426f1392a9d99e38e7ee Mon Sep 17 00:00:00 2001 From: maruyama Date: Mon, 21 Sep 2026 01:21:13 +0900 Subject: [PATCH 26/26] fix : clarify RoundingMode fallback --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 1773d89964b..b1dfe7f63bc 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -122,8 +122,10 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, /** * @return 1|2|3|4|null - * // Fall back to the default return type for RoundingMode cases - * // without a corresponding PHP_ROUND_HALF_* constant. + * + * Returns null for RoundingMode cases without a corresponding + * PHP_ROUND_HALF_* constant, so constant evaluation falls back + * to the existing return-type logic. */ private function resolveRoundMode(Type $modeType): ?int {