From ae3b3403616d0e40291bc27269123f8be52eab0a Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 23 Oct 2021 22:14:15 +0200 Subject: [PATCH 01/17] These functions can also return boolean false. --- conf/config.neon | 5 +++ resources/functionMap.php | 6 +-- .../Php/RoundFunctionReturnTypeExtension.php | 44 +++++++++++++++++++ 3 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 src/Type/Php/RoundFunctionReturnTypeExtension.php diff --git a/conf/config.neon b/conf/config.neon index 1cc26c1664..0d954bd6b1 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1305,6 +1305,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\RoundFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\StrtotimeFunctionReturnTypeExtension tags: diff --git a/resources/functionMap.php b/resources/functionMap.php index f55becba80..959f9866c2 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -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'], @@ -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'], @@ -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'], diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..e906aa918f --- /dev/null +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -0,0 +1,44 @@ +getName(), + [ + 'round', + 'ceil', + 'floor', + ] + ); + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $defaultReturnType = new FloatType(); + if (count($functionCall->getArgs()) < 1) { + return $defaultReturnType; + } + + $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + if ($firstArgType instanceof ArrayType) { + return new ConstantBooleanType( false ); + } + + return $defaultReturnType; + } + +} From 95aa585e96e03e2bd4370bf7712e4f8554378c0e Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 23 Oct 2021 22:21:35 +0200 Subject: [PATCH 02/17] Add tests for rounding function return types. --- .../Analyser/NodeScopeResolverTest.php | 1 + tests/PHPStan/Analyser/data/round.php | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/PHPStan/Analyser/data/round.php diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 97bbc2d1fb..dfa6b837a9 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -551,6 +551,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5992.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6001.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/round.php'); if (PHP_VERSION_ID >= 80100) { yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5287-php81.php'); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php new file mode 100644 index 0000000000..017f7f2270 --- /dev/null +++ b/tests/PHPStan/Analyser/data/round.php @@ -0,0 +1,26 @@ + Date: Sat, 23 Oct 2021 22:29:04 +0200 Subject: [PATCH 03/17] Use strict comparison in `in_array()`. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index e906aa918f..8d3751cd1c 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -22,7 +22,8 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'round', 'ceil', 'floor', - ] + ], + true ); } From 41ad8cd98f024cfab2f7d092a3ad25b418f8cbf5 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 23 Oct 2021 22:31:35 +0200 Subject: [PATCH 04/17] Coding standards fixes. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 8d3751cd1c..45ff8ff418 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -6,9 +6,9 @@ use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; -use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Type; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -36,7 +36,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); if ($firstArgType instanceof ArrayType) { - return new ConstantBooleanType( false ); + return new ConstantBooleanType(false); } return $defaultReturnType; From cb697fb746ec11353f26d9ad7bf69e9066861705 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sat, 23 Oct 2021 22:39:07 +0200 Subject: [PATCH 05/17] More test cases. --- tests/PHPStan/Analyser/data/round.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php index 017f7f2270..abc5c2e9db 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/data/round.php @@ -9,6 +9,11 @@ assertType('float', round(123.456)); 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())); // Ceil @@ -16,6 +21,11 @@ assertType('float', ceil(123.456)); 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())); // Floor @@ -23,4 +33,9 @@ assertType('float', floor(123.456)); 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())); From c081b6cb20c9b9881e8aceb2dbacc09aab3777f0 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 24 Oct 2021 13:45:46 +0200 Subject: [PATCH 06/17] Account for all array types. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 2 +- tests/PHPStan/Analyser/data/round.php | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 45ff8ff418..358dcc07c1 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -35,7 +35,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); - if ($firstArgType instanceof ArrayType) { + if ($firstArgType->isArray()->yes()) { return new ConstantBooleanType(false); } diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php index abc5c2e9db..664a8368d7 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/data/round.php @@ -15,6 +15,7 @@ assertType('float', round(new \stdClass)); assertType('float', round('')); assertType('false', round(array())); +assertType('false', round(array(123))); // Ceil assertType('float', ceil(123)); @@ -27,6 +28,7 @@ assertType('float', ceil(new \stdClass)); assertType('float', ceil('')); assertType('false', ceil(array())); +assertType('false', ceil(array(123))); // Floor assertType('float', floor(123)); @@ -39,3 +41,4 @@ assertType('float', floor(new \stdClass)); assertType('float', floor('')); assertType('false', floor(array())); +assertType('false', floor(array(123))); From dab8b0f32cf281410318764dd08e7bfc2bddf00c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 24 Oct 2021 14:25:48 +0200 Subject: [PATCH 07/17] Add handling for different behaviour of the rounding functions in PHP 8. --- .../Php/RoundFunctionReturnTypeExtension.php | 10 ++++- .../Analyser/NodeScopeResolverTest.php | 7 ++- tests/PHPStan/Analyser/data/round-php8.php | 44 +++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/round-php8.php diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 358dcc07c1..f7168cc945 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -5,10 +5,11 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; use PHPStan\Type\FloatType; +use PHPStan\Type\IntegerType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -35,6 +36,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + + if (PHP_VERSION_ID >= 80000) { + if (!($firstArgType instanceof IntegerType) && !($firstArgType instanceof FloatType)) { + return new NeverType(true); + } + } + if ($firstArgType->isArray()->yes()) { return new ConstantBooleanType(false); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index dfa6b837a9..8f8cde0912 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -551,7 +551,12 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5992.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6001.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/round.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'); diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php new file mode 100644 index 0000000000..8eb67708be --- /dev/null +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -0,0 +1,44 @@ + Date: Sun, 24 Oct 2021 14:31:44 +0200 Subject: [PATCH 08/17] Correct the never type. --- tests/PHPStan/Analyser/data/round-php8.php | 54 +++++++++++----------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php index 8eb67708be..0194620446 100644 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -7,38 +7,38 @@ // Round assertType('float', round(123)); assertType('float', round(123.456)); -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('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))); // Ceil assertType('float', ceil(123)); assertType('float', ceil(123.456)); -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('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))); // Floor assertType('float', floor(123)); assertType('float', floor(123.456)); -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('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))); From 1b705e832a9ccbac66729d73f2dc1ac8c4b6101c Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 24 Oct 2021 14:43:11 +0200 Subject: [PATCH 09/17] Correct the return type when no parameter is passed. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 7 ++++++- tests/PHPStan/Analyser/data/round-php8.php | 3 +++ tests/PHPStan/Analyser/data/round.php | 3 +++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index f7168cc945..82d7319726 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -10,6 +10,7 @@ use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; use PHPStan\Type\NeverType; +use PHPStan\Type\NullType; use PHPStan\Type\Type; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension @@ -32,7 +33,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, { $defaultReturnType = new FloatType(); if (count($functionCall->getArgs()) < 1) { - return $defaultReturnType; + if (PHP_VERSION_ID >= 80000) { + return new NeverType(true); + } else { + return new NullType(); + } } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php index 0194620446..64899f8bd9 100644 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -16,6 +16,7 @@ assertType('*NEVER*', round('')); assertType('*NEVER*', round(array())); assertType('*NEVER*', round(array(123))); +assertType('*NEVER*', round()); // Ceil assertType('float', ceil(123)); @@ -29,6 +30,7 @@ assertType('*NEVER*', ceil('')); assertType('*NEVER*', ceil(array())); assertType('*NEVER*', ceil(array(123))); +assertType('*NEVER*', ceil()); // Floor assertType('float', floor(123)); @@ -42,3 +44,4 @@ assertType('*NEVER*', floor('')); assertType('*NEVER*', floor(array())); assertType('*NEVER*', floor(array(123))); +assertType('*NEVER*', floor()); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php index 664a8368d7..0bf9f22aad 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/data/round.php @@ -16,6 +16,7 @@ assertType('float', round('')); assertType('false', round(array())); assertType('false', round(array(123))); +assertType('null', round()); // Ceil assertType('float', ceil(123)); @@ -29,6 +30,7 @@ assertType('float', ceil('')); assertType('false', ceil(array())); assertType('false', ceil(array(123))); +assertType('null', ceil()); // Floor assertType('float', floor(123)); @@ -42,3 +44,4 @@ assertType('float', floor('')); assertType('false', floor(array())); assertType('false', floor(array(123))); +assertType('null', floor()); From 377b0a085a1a7f83676a43161673a51cf78e1876 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Sun, 24 Oct 2021 14:57:42 +0200 Subject: [PATCH 10/17] Correctly account for an unknown parameter type. --- .../Php/RoundFunctionReturnTypeExtension.php | 39 ++++++++++++++----- tests/PHPStan/Analyser/data/round-php8.php | 3 ++ tests/PHPStan/Analyser/data/round.php | 3 ++ 3 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 82d7319726..0af6c90500 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -5,10 +5,12 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; 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; @@ -31,28 +33,45 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - $defaultReturnType = new FloatType(); + if (PHP_VERSION_ID >= 80000) { + // PHP 8 fatals with a missing parameter. + $noArgsReturnType = new NeverType(true); + // PHP 7 can return either 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) { - if (PHP_VERSION_ID >= 80000) { - return new NeverType(true); - } else { - return new NullType(); - } + return $noArgsReturnType; } $firstArgType = $scope->getType($functionCall->getArgs()[0]->value); + if ($firstArgType instanceof MixedType) { + return $defaultReturnType; + } + if (PHP_VERSION_ID >= 80000) { if (!($firstArgType instanceof IntegerType) && !($firstArgType instanceof FloatType)) { + // PHP 8 fatals if the parameter is not an integer or float. return new NeverType(true); } - } - - if ($firstArgType->isArray()->yes()) { + } elseif ($firstArgType->isArray()->yes()) { + // PHP 7 returns false if the parameter is an array. return new ConstantBooleanType(false); } - return $defaultReturnType; + return new FloatType(); } } diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php index 64899f8bd9..8802b2034b 100644 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -17,6 +17,7 @@ assertType('*NEVER*', round(array())); assertType('*NEVER*', round(array(123))); assertType('*NEVER*', round()); +assertType('(*NEVER*|float)', round($_GET['foo'])); // Ceil assertType('float', ceil(123)); @@ -31,6 +32,7 @@ assertType('*NEVER*', ceil(array())); assertType('*NEVER*', ceil(array(123))); assertType('*NEVER*', ceil()); +assertType('(*NEVER*|float)', ceil($_GET['foo'])); // Floor assertType('float', floor(123)); @@ -45,3 +47,4 @@ assertType('*NEVER*', floor(array())); assertType('*NEVER*', floor(array(123))); assertType('*NEVER*', floor()); +assertType('(*NEVER*|float)', floor($_GET['foo'])); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php index 0bf9f22aad..7c815d29d5 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/data/round.php @@ -17,6 +17,7 @@ assertType('false', round(array())); assertType('false', round(array(123))); assertType('null', round()); +assertType('(float|false)', round($_GET['foo'])); // Ceil assertType('float', ceil(123)); @@ -31,6 +32,7 @@ assertType('false', ceil(array())); assertType('false', ceil(array(123))); assertType('null', ceil()); +assertType('(float|false)', ceil($_GET['foo'])); // Floor assertType('float', floor(123)); @@ -45,3 +47,4 @@ assertType('false', floor(array())); assertType('false', floor(array(123))); assertType('null', floor()); +assertType('(float|false)', floor($_GET['foo'])); From abf584ece5fda3f4d21a940216e59f6d11392681 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 1 Feb 2022 14:55:45 +0000 Subject: [PATCH 11/17] Fix a typo. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 0af6c90500..b092734785 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -36,7 +36,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (PHP_VERSION_ID >= 80000) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); - // PHP 7 can return either a float or fatal. + // PHP 8 can return either a float or fatal. $defaultReturnType = new BenevolentUnionType([ new FloatType(), new NeverType(true), From bb03562c56b70273e4513172cabb7213c0abb574 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 1 Feb 2022 20:42:55 +0000 Subject: [PATCH 12/17] Add tests for `float|int` union type. --- tests/PHPStan/Analyser/data/round-php8.php | 3 +++ tests/PHPStan/Analyser/data/round.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/PHPStan/Analyser/data/round-php8.php b/tests/PHPStan/Analyser/data/round-php8.php index 8802b2034b..68a0c97768 100644 --- a/tests/PHPStan/Analyser/data/round-php8.php +++ b/tests/PHPStan/Analyser/data/round-php8.php @@ -7,6 +7,7 @@ // 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)); @@ -22,6 +23,7 @@ // 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)); @@ -37,6 +39,7 @@ // 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)); diff --git a/tests/PHPStan/Analyser/data/round.php b/tests/PHPStan/Analyser/data/round.php index 7c815d29d5..1cbb06d2cf 100644 --- a/tests/PHPStan/Analyser/data/round.php +++ b/tests/PHPStan/Analyser/data/round.php @@ -7,6 +7,7 @@ // 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)); @@ -22,6 +23,7 @@ // 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)); @@ -37,6 +39,7 @@ // 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)); From d071c3ec4b0b1ed0204faecfc21878ecafaef7af Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Tue, 1 Feb 2022 22:11:12 +0000 Subject: [PATCH 13/17] Handle an integer and float union. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index b092734785..1bbe89ce75 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -14,6 +14,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\NullType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -62,7 +63,11 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, } if (PHP_VERSION_ID >= 80000) { - if (!($firstArgType instanceof IntegerType) && !($firstArgType instanceof FloatType)) { + $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); } From 87d286fd8e0f7a458718e676c4275c51cf6bf384 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 2 Feb 2022 01:56:27 +0000 Subject: [PATCH 14/17] Docs. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 1bbe89ce75..fc617dccc0 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -37,7 +37,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if (PHP_VERSION_ID >= 80000) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); - // PHP 8 can return either a float or fatal. + // PHP 8 can either return a float or fatal. $defaultReturnType = new BenevolentUnionType([ new FloatType(), new NeverType(true), From 3a486a9c2493ee472b9dbb28594586512133359d Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 2 Feb 2022 02:06:17 +0000 Subject: [PATCH 15/17] Coding standards. --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 5 ++++- tests/PHPStan/Analyser/NodeScopeResolverTest.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index fc617dccc0..858133aac5 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -15,6 +15,9 @@ use PHPStan\Type\NullType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function count; +use function in_array; +use const PHP_VERSION_ID; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -28,7 +31,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo 'ceil', 'floor', ], - true + true, ); } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 8f8cde0912..aaf6bc04b3 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -551,7 +551,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5017.php'); 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 { From d916dd300276a424c1f6b9d902eb9996833f7d0f Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 2 Feb 2022 12:45:37 +0000 Subject: [PATCH 16/17] Switch to injecting the PHP version logic. --- src/Php/PhpVersion.php | 5 +++++ src/Type/Php/RoundFunctionReturnTypeExtension.php | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index a1fc40fe26..b236a79122 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -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; diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 858133aac5..2808e33c28 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ 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; @@ -17,10 +18,12 @@ use PHPStan\Type\TypeCombinator; use function count; use function in_array; -use const PHP_VERSION_ID; class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } public function isFunctionSupported(FunctionReflection $functionReflection): bool { @@ -37,7 +40,7 @@ public function isFunctionSupported(FunctionReflection $functionReflection): boo public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type { - if (PHP_VERSION_ID >= 80000) { + if ($this->phpVersion->hasStricterRoundFunctions()) { // PHP 8 fatals with a missing parameter. $noArgsReturnType = new NeverType(true); // PHP 8 can either return a float or fatal. @@ -65,7 +68,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $defaultReturnType; } - if (PHP_VERSION_ID >= 80000) { + if ($this->phpVersion->hasStricterRoundFunctions()) { $allowed = TypeCombinator::union( new IntegerType(), new FloatType(), From eaad325f6ad3debd913ddd75a316b4ab9223650d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 2 Feb 2022 23:07:19 +0100 Subject: [PATCH 17/17] Fix CS --- src/Type/Php/RoundFunctionReturnTypeExtension.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Type/Php/RoundFunctionReturnTypeExtension.php b/src/Type/Php/RoundFunctionReturnTypeExtension.php index 2808e33c28..4d75ec260f 100644 --- a/src/Type/Php/RoundFunctionReturnTypeExtension.php +++ b/src/Type/Php/RoundFunctionReturnTypeExtension.php @@ -21,6 +21,7 @@ class RoundFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) { }