From a35b4b714d6f2ae646c9b0ef22c2b0e6b67b4f0a Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:10:15 +0900 Subject: [PATCH 1/9] RoundFunctionReturnTypeExtension supports types with ConstantScalar --- .../Php/RoundFunctionReturnTypeExtension.php | 101 +++++++++++++++++- tests/PHPStan/Analyser/nsrt/round-php8.php | 39 +++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 8d064c1ef3..5041b0d6c3 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -9,6 +9,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 +20,13 @@ 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 OffsetAccessLegal\float; +use function is_int; +use function is_float; final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -56,22 +62,62 @@ 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; } + //この条件分岐はバージョン情報 if ($this->phpVersion->hasStricterRoundFunctions()) { + + //PHP言語仕様として、引数として指定されてるIntegerTypeとFloatTypeを指定 $allowed = TypeCombinator::union( new IntegerType(), new FloatType(), ); + //厳密な型を宣言しないならとういう条件分岐 if (!$scope->isDeclareStrictTypes()) { $allowed = TypeCombinator::union( $allowed, @@ -84,16 +130,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 54836b7623..86fd4ee8e4 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 1c0c0cb1651e14d05d3d2668d5466a1a269e1abb Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:51:12 +0900 Subject: [PATCH 2/9] fix RoundFunctionReturnTypeExtension --- .../Php/RoundFunctionReturnTypeExtension.php | 63 +++++++++---------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 5041b0d6c3..ea5084ebcc 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\Php\PhpVersion; @@ -20,13 +21,14 @@ 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; -use function OffsetAccessLegal\float; -use function is_int; use function is_float; +use function is_int; +use function round; final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -69,7 +71,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $noArgsReturnType; } - $argType = $scope->getType($args[0]->value); $functionName = $functionReflection->getName(); @@ -78,7 +79,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ($proc !== null) { $constantScalarValues = $argType->getConstantScalarValues(); - $rv = array(); + $rv = []; foreach ($constantScalarValues as $constantScalarValue) { @@ -92,14 +93,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); @@ -130,15 +130,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 を返します。 @@ -149,50 +146,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 d64878d1d22753c4fa04298c099c4c95800f496a Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 10:53:29 +0900 Subject: [PATCH 3/9] 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 86fd4ee8e4..3d841d9238 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 05dc61fe1f1e18712a26d9b99a4969800dd772ba Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 11:04:19 +0900 Subject: [PATCH 4/9] fix style --- .../Php/RoundFunctionReturnTypeExtension.php | 32 ++++++------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index ea5084ebcc..df175b5912 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\Php\PhpVersion; @@ -66,58 +67,44 @@ 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()) { - - //PHP言語仕様として、引数として指定されてるIntegerTypeとFloatTypeを指定 $allowed = TypeCombinator::union( new IntegerType(), new FloatType(), ); - //厳密な型を宣言しないならとういう条件分岐 if (!$scope->isDeclareStrictTypes()) { $allowed = TypeCombinator::union( $allowed, @@ -130,7 +117,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); @@ -138,7 +124,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); } @@ -146,16 +131,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); @@ -185,6 +172,7 @@ public function getProc(string $functionName, array $args, Scope $scope): ?Closu return static fn ($name) => round($name, $precision, $mode[0]); } } + return null; } From 465d28ab5478369b0843b12c125c3747ba344855 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 12:15:04 +0900 Subject: [PATCH 5/9] 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 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 3d841d9238..f128dbb6e3 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 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)); +} From a6c09f3b63a8689b65ba516d4a0ebb3437e03073 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 13:45:29 +0900 Subject: [PATCH 6/9] 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 df175b5912..dd773d30ed 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -151,10 +151,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; @@ -168,7 +167,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 d7aadbd295e736147a69f2a7df4a48b36c4017f8 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 14:01:39 +0900 Subject: [PATCH 7/9] 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 dd773d30ed..c1a8a6dd9a 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; @@ -71,34 +70,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()) { $allowed = TypeCombinator::union( new IntegerType(), @@ -133,46 +115,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 27b10db257f48d24204ca28ec47679b6e912940d Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 14:13:23 +0900 Subject: [PATCH 8/9] 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 c1a8a6dd9a..09e0b226d2 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -29,6 +29,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; final class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -146,7 +150,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 e4ad0979ce9975e822aa96de6d766c8d7bddbac5 Mon Sep 17 00:00:00 2001 From: shiomachi Date: Mon, 26 Aug 2024 18:44:30 +0900 Subject: [PATCH 9/9] fix --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 09e0b226d2..9de365fcfa 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -21,7 +21,6 @@ use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; -use function array_map; use function ceil; use function count; use function floor; @@ -107,7 +106,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); @@ -174,7 +172,7 @@ 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;