From b095f446e1e4e0f2e0dab0110c0879d6a1864ad5 Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 16 Jan 2022 21:33:04 +0100 Subject: [PATCH] Cherry-pick: Try to provide literal int types when possible (fixes #6966) (#7071) * Fixed vimeo/psalm#6966 * Only accept >= 0 values for mode argument in round() * Made round() only return float or literal float values and remove unneeded test * Registered RoundReturnTypeProvider * Updated cast analyzer to handle single string literal int values as literal ints * Fixed psalm errors * Fix invalid property accesses * Addressed comments * Added Tests * Marked RoundReturnTypeProvider as internal * Fixed CS --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- .../Provider/FunctionReturnTypeProvider.php | 2 + .../RoundReturnTypeProvider.php | 78 +++++++++++++++++++ tests/FunctionCallTest.php | 9 ++- tests/TypeReconciliation/ValueTest.php | 8 ++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 1d89ed805ae..01a68621f09 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11714,7 +11714,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], -'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 79545d9053f..370b989cf88 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14751,7 +14751,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], - 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], + 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 8b124ea5532..e4636d5c3a0 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -35,6 +35,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\MktimeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider; @@ -109,6 +110,7 @@ public function __construct() $this->registerClass(TriggerErrorReturnTypeProvider::class); $this->registerClass(RandReturnTypeProvider::class); $this->registerClass(InArrayReturnTypeProvider::class); + $this->registerClass(RoundReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php new file mode 100644 index 00000000000..eb8ee561f54 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php @@ -0,0 +1,78 @@ + + */ + public static function getFunctionIds(): array + { + return ['round']; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union + { + $call_args = $event->getCallArgs(); + if (count($call_args) === 0) { + return null; + } + + $statements_source = $event->getStatementsSource(); + $nodeTypeProvider = $statements_source->getNodeTypeProvider(); + + $num_arg = $nodeTypeProvider->getType($call_args[0]->value); + + $precision_val = 0; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 1) { + $type = $statements_source->node_data->getType($call_args[1]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + $precision_val = $atomic_type->value; + } + } + } + + $mode_val = PHP_ROUND_HALF_UP; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 2) { + $type = $statements_source->node_data->getType($call_args[2]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + /** @var positive-int|0 $mode_val */ + $mode_val = $atomic_type->value; + } + } + } + + if ($num_arg !== null && $num_arg->isSingle()) { + $num_type = array_values($num_arg->getAtomicTypes())[0]; + if ($num_type instanceof Type\Atomic\TLiteralFloat || $num_type instanceof Type\Atomic\TLiteralInt) { + $rounded_val = round($num_type->value, $precision_val, $mode_val); + return new Type\Union([new Type\Atomic\TLiteralFloat($rounded_val)]); + } + } + + return new Type\Union([new Type\Atomic\TFloat()]); + } +} diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index be4f4c32b17..74da19ababf 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -31,7 +31,6 @@ function filter(array $strings): array { } ' ], - 'typedArrayWithDefault' => [ ' 'lowercase-string', ], ], + 'round_literalValue' => [ + ' [ + '$a===' => 'float(10.36)', + ], + ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index b5963ac4125..cda14767394 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -909,6 +909,14 @@ function foo(string $s) : void { $interval = \DateInterval::createFromDateString("30 дней"); if ($interval === false) {}', ], + 'literalInt' => [ + ' [ + '$a===' => '5', + ], + ], ]; }