From 26dd4c5b8fcd1619fac72104e654442b659e36ea Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 16 Jan 2022 21:33:04 +0100 Subject: [PATCH] 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 +- .../Statements/Expression/CastAnalyzer.php | 33 ++++---- .../Provider/FunctionReturnTypeProvider.php | 2 + .../RoundReturnTypeProvider.php | 78 +++++++++++++++++++ tests/FunctionCallTest.php | 8 ++ tests/TypeReconciliation/ValueTest.php | 8 ++ 7 files changed, 111 insertions(+), 22 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 9f31a7a44..312a282ce 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11670,7 +11670,7 @@ return [ '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 362a181db..aed823b6d 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14709,7 +14709,7 @@ return [ '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/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 3d34b3f44..a92bc92e7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -22,7 +22,6 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; @@ -64,8 +63,8 @@ class CastAnalyzer return false; } - $as_int = true; $valid_int_type = null; + $type_parent_nodes = null; $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { @@ -74,36 +73,30 @@ class CastAnalyzer if (!$maybe_type->from_calculation) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } + } elseif ($maybe_type->isSingleStringLiteral()) { + $valid_int_type = Type::getInt(false, (int)$maybe_type->getSingleStringLiteral()->value); } if (count($maybe_type->getAtomicTypes()) === 1 - && $maybe_type->getSingleAtomic() instanceof TBool) { - $as_int = false; - $type = new Union([ + && $maybe_type->getSingleAtomic() instanceof Type\Atomic\TBool) { + $valid_int_type = new Union([ new TLiteralInt(0), new TLiteralInt(1), ]); + } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes; - } - - $statements_analyzer->node_data->setType($stmt, $type); + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $type_parent_nodes = $maybe_type->parent_nodes; } } - if ($as_int) { - $type = $valid_int_type ?? Type::getInt(); - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; - } - - $statements_analyzer->node_data->setType($stmt, $type); + $type = $valid_int_type ?? Type::getInt(); + if ($type_parent_nodes !== null) { + $type->parent_nodes = $type_parent_nodes; } + $statements_analyzer->node_data->setType($stmt, $type); + return true; } diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 06390ab80..cb8dc08ae 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -34,6 +34,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\MinMaxReturnTypeProvider; 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; @@ -95,6 +96,7 @@ class FunctionReturnTypeProvider $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 000000000..eb8ee561f --- /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 7bacb7362..00fb3f40f 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1785,6 +1785,14 @@ class FunctionCallTest extends TestCase 'ignored_issues' => [], 'php_version' => '8.0', ], + 'round_literalValue' => [ + 'code' => ' [ + '$a===' => 'float(10.36)', + ], + ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index 5a4ad3b3d..3a0e12963 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -904,6 +904,14 @@ class ValueTest extends TestCase if (empty($s)) {} }', ], + 'literalInt' => [ + 'code' => ' [ + '$a===' => '5', + ], + ], ]; }