Skip to content

Commit

Permalink
Fix the return types of round()/ceil()/floor()
Browse files Browse the repository at this point in the history
  • Loading branch information
johnbillion committed Feb 3, 2022
1 parent aaa8db3 commit afc49fd
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 3 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -1305,6 +1305,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\RoundFunctionReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\StrtotimeFunctionReturnTypeExtension
tags:
Expand Down
6 changes: 3 additions & 3 deletions resources/functionMap.php
Expand Up @@ -932,7 +932,7 @@
'CallbackFilterIterator::next' => ['void'],
'CallbackFilterIterator::rewind' => ['void'],
'CallbackFilterIterator::valid' => ['bool'],
'ceil' => ['float', 'number'=>'float'],
'ceil' => ['float|false', 'number'=>'float'],
'chdb::__construct' => ['void', 'pathname'=>'string'],
'chdb::get' => ['string', 'key'=>'string'],
'chdb_create' => ['bool', 'pathname'=>'string', 'data'=>'array'],
Expand Down Expand Up @@ -2990,7 +2990,7 @@
'finfo_set_flags' => ['bool', 'finfo'=>'resource', 'options'=>'int'],
'floatval' => ['float', 'var'=>'mixed'],
'flock' => ['bool', 'fp'=>'resource', 'operation'=>'int', '&w_wouldblock='=>'int'],
'floor' => ['float', 'number'=>'float'],
'floor' => ['float|false', 'number'=>'float'],
'flush' => ['void'],
'fmod' => ['float', 'x'=>'float', 'y'=>'float'],
'fnmatch' => ['bool', 'pattern'=>'string', 'filename'=>'string', 'flags='=>'int'],
Expand Down Expand Up @@ -9889,7 +9889,7 @@
'rewind' => ['bool', 'fp'=>'resource'],
'rewinddir' => ['null|false', 'dir_handle='=>'resource'],
'rmdir' => ['bool', 'dirname'=>'string', 'context='=>'resource'],
'round' => ['float', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'],
'round' => ['float|false', 'number'=>'float', 'precision='=>'int', 'mode='=>'int'],
'rpm_close' => ['bool', 'rpmr'=>'resource'],
'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'],
'rpm_is_valid' => ['bool', 'filename'=>'string'],
Expand Down
5 changes: 5 additions & 0 deletions src/Php/PhpVersion.php
Expand Up @@ -156,6 +156,11 @@ public function supportsCaseInsensitiveConstantNames(): bool
return $this->versionId < 80000;
}

public function hasStricterRoundFunctions(): bool
{
return $this->versionId >= 80000;
}

public function hasTentativeReturnTypes(): bool
{
return $this->versionId >= 80100;
Expand Down
89 changes: 89 additions & 0 deletions src/Type/Php/RoundFunctionReturnTypeExtension.php
@@ -0,0 +1,89 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;

class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private PhpVersion $phpVersion)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return in_array(
$functionReflection->getName(),
[
'round',
'ceil',
'floor',
],
true,
);
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if ($this->phpVersion->hasStricterRoundFunctions()) {
// PHP 8 fatals with a missing parameter.
$noArgsReturnType = new NeverType(true);
// PHP 8 can either return a float or fatal.
$defaultReturnType = new BenevolentUnionType([
new FloatType(),
new NeverType(true),
]);
} else {
// PHP 7 returns null with a missing parameter.
$noArgsReturnType = new NullType();
// PHP 7 can return either a float or false.
$defaultReturnType = new BenevolentUnionType([
new FloatType(),
new ConstantBooleanType(false),
]);
}

if (count($functionCall->getArgs()) < 1) {
return $noArgsReturnType;
}

$firstArgType = $scope->getType($functionCall->getArgs()[0]->value);

if ($firstArgType instanceof MixedType) {
return $defaultReturnType;
}

if ($this->phpVersion->hasStricterRoundFunctions()) {
$allowed = TypeCombinator::union(
new IntegerType(),
new FloatType(),
);
if (!$allowed->accepts($firstArgType, true)->yes()) {
// 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);
}

return new FloatType();
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -552,6 +552,12 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5992.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6001.php');

if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/round-php8.php');
} else {
yield from $this->gatherAssertTypes(__DIR__ . '/data/round.php');
}

if (PHP_VERSION_ID >= 80100) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287-php81.php');
} else {
Expand Down
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/round-php8.php
@@ -0,0 +1,53 @@
<?php

namespace RoundFamilyTestPHP8;

use function PHPStan\Testing\assertType;

// Round
assertType('float', round(123));
assertType('float', round(123.456));
assertType('float', round($_GET['foo'] / 60));
assertType('*NEVER*', round('123'));
assertType('*NEVER*', round('123.456'));
assertType('*NEVER*', round(null));
assertType('*NEVER*', round(true));
assertType('*NEVER*', round(false));
assertType('*NEVER*', round(new \stdClass));
assertType('*NEVER*', round(''));
assertType('*NEVER*', round(array()));
assertType('*NEVER*', round(array(123)));
assertType('*NEVER*', round());
assertType('(*NEVER*|float)', round($_GET['foo']));

// Ceil
assertType('float', ceil(123));
assertType('float', ceil(123.456));
assertType('float', ceil($_GET['foo'] / 60));
assertType('*NEVER*', ceil('123'));
assertType('*NEVER*', ceil('123.456'));
assertType('*NEVER*', ceil(null));
assertType('*NEVER*', ceil(true));
assertType('*NEVER*', ceil(false));
assertType('*NEVER*', ceil(new \stdClass));
assertType('*NEVER*', ceil(''));
assertType('*NEVER*', ceil(array()));
assertType('*NEVER*', ceil(array(123)));
assertType('*NEVER*', ceil());
assertType('(*NEVER*|float)', ceil($_GET['foo']));

// Floor
assertType('float', floor(123));
assertType('float', floor(123.456));
assertType('float', floor($_GET['foo'] / 60));
assertType('*NEVER*', floor('123'));
assertType('*NEVER*', floor('123.456'));
assertType('*NEVER*', floor(null));
assertType('*NEVER*', floor(true));
assertType('*NEVER*', floor(false));
assertType('*NEVER*', floor(new \stdClass));
assertType('*NEVER*', floor(''));
assertType('*NEVER*', floor(array()));
assertType('*NEVER*', floor(array(123)));
assertType('*NEVER*', floor());
assertType('(*NEVER*|float)', floor($_GET['foo']));
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/round.php
@@ -0,0 +1,53 @@
<?php

namespace RoundFamilyTest;

use function PHPStan\Testing\assertType;

// Round
assertType('float', round(123));
assertType('float', round(123.456));
assertType('float', round($_GET['foo'] / 60));
assertType('float', round('123'));
assertType('float', round('123.456'));
assertType('float', round(null));
assertType('float', round(true));
assertType('float', round(false));
assertType('float', round(new \stdClass));
assertType('float', round(''));
assertType('false', round(array()));
assertType('false', round(array(123)));
assertType('null', round());
assertType('(float|false)', round($_GET['foo']));

// Ceil
assertType('float', ceil(123));
assertType('float', ceil(123.456));
assertType('float', ceil($_GET['foo'] / 60));
assertType('float', ceil('123'));
assertType('float', ceil('123.456'));
assertType('float', ceil(null));
assertType('float', ceil(true));
assertType('float', ceil(false));
assertType('float', ceil(new \stdClass));
assertType('float', ceil(''));
assertType('false', ceil(array()));
assertType('false', ceil(array(123)));
assertType('null', ceil());
assertType('(float|false)', ceil($_GET['foo']));

// Floor
assertType('float', floor(123));
assertType('float', floor(123.456));
assertType('float', floor($_GET['foo'] / 60));
assertType('float', floor('123'));
assertType('float', floor('123.456'));
assertType('float', floor(null));
assertType('float', floor(true));
assertType('float', floor(false));
assertType('float', floor(new \stdClass));
assertType('float', floor(''));
assertType('false', floor(array()));
assertType('false', floor(array(123)));
assertType('null', floor());
assertType('(float|false)', floor($_GET['foo']));

0 comments on commit afc49fd

Please sign in to comment.