Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to provide literal int types when possible (fixes #6966) #7071

2 changes: 1 addition & 1 deletion dictionaries/CallMap.php
Expand Up @@ -11670,7 +11670,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'],
Expand Down
2 changes: 1 addition & 1 deletion dictionaries/CallMap_historical.php
Expand Up @@ -14709,7 +14709,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'],
Expand Down
33 changes: 13 additions & 20 deletions src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php
Expand Up @@ -22,7 +22,6 @@
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;
Expand Down Expand Up @@ -64,8 +63,8 @@ public static function analyze(
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) {
Expand All @@ -74,36 +73,30 @@ public static function analyze(
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 ($as_int) {
$type = $valid_int_type ?? Type::getInt();

if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
) {
$type->parent_nodes = $maybe_type->parent_nodes ?? [];
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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Expand Up @@ -34,6 +34,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;
Expand Down Expand Up @@ -95,6 +96,7 @@ public function __construct()
$this->registerClass(TriggerErrorReturnTypeProvider::class);
$this->registerClass(RandReturnTypeProvider::class);
$this->registerClass(InArrayReturnTypeProvider::class);
$this->registerClass(RoundReturnTypeProvider::class);
}

/**
Expand Down
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;

use function array_values;
use function count;
use function round;

use const PHP_ROUND_HALF_UP;

/**
* @internal
*/
class RoundReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
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()]);
}
}
8 changes: 8 additions & 0 deletions tests/FunctionCallTest.php
Expand Up @@ -1785,6 +1785,14 @@ function sayHello(string $needle): void {
'ignored_issues' => [],
'php_version' => '8.0',
],
'round_literalValue' => [
'code' => '<?php
$a = round(10.363, 2);
',
'assertions' => [
'$a===' => 'float(10.36)',
],
],
];
}

Expand Down
8 changes: 8 additions & 0 deletions tests/TypeReconciliation/ValueTest.php
Expand Up @@ -904,6 +904,14 @@ function foo(string $s) : void {
if (empty($s)) {}
}',
],
'literalInt' => [
'code' => '<?php
$a = (int)"5";
',
'assertions' => [
'$a===' => '5',
],
],
];
}

Expand Down