From 7e57ca0fe3c5f840488a40b41de82f9b581c6319 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Thu, 16 Jan 2020 21:48:57 +0000 Subject: [PATCH] random_int() return type and parameters rule --- conf/bleedingEdge.neon | 3 + conf/config.level5.neon | 12 +++ conf/config.neon | 5 ++ .../Functions/RandomIntParametersRule.php | 84 +++++++++++++++++++ .../RandomIntFunctionReturnTypeExtension.php | 63 ++++++++++++++ .../Analyser/NodeScopeResolverTest.php | 6 ++ tests/PHPStan/Analyser/data/random-int.php | 39 +++++++++ tests/PHPStan/Levels/data/acceptTypes-5.json | 15 ++++ tests/PHPStan/Levels/data/acceptTypes-7.json | 15 ++++ tests/PHPStan/Levels/data/acceptTypes.php | 31 +++++++ .../Functions/RandomIntParametersRuleTest.php | 58 +++++++++++++ .../Rules/Functions/data/random-int.php | 33 ++++++++ 12 files changed, 364 insertions(+) create mode 100644 src/Rules/Functions/RandomIntParametersRule.php create mode 100644 src/Type/Php/RandomIntFunctionReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/data/random-int.php create mode 100644 tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/random-int.php diff --git a/conf/bleedingEdge.neon b/conf/bleedingEdge.neon index e69de29bb2..f0d372d9b9 100644 --- a/conf/bleedingEdge.neon +++ b/conf/bleedingEdge.neon @@ -0,0 +1,3 @@ +parameters: + featureToggles: + randomIntParameters: true diff --git a/conf/config.level5.neon b/conf/config.level5.neon index 8ebba4d161..16249f01fe 100644 --- a/conf/config.level5.neon +++ b/conf/config.level5.neon @@ -1,6 +1,18 @@ includes: - config.level4.neon +conditionalTags: + PHPStan\Rules\Functions\RandomIntParametersRule: + phpstan.rules.rule: %featureToggles.randomIntParameters% + parameters: checkFunctionArgumentTypes: true checkArgumentsPassedByReference: true + featureToggles: + randomIntParameters: false + +services: + - + class: PHPStan\Rules\Functions\RandomIntParametersRule + arguments: + reportMaybes: %reportMaybes% diff --git a/conf/config.neon b/conf/config.neon index 1fae23f351..e4c812d9b5 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -717,6 +717,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\RandomIntFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\RangeFunctionReturnTypeExtension tags: diff --git a/src/Rules/Functions/RandomIntParametersRule.php b/src/Rules/Functions/RandomIntParametersRule.php new file mode 100644 index 0000000000..0b944275b4 --- /dev/null +++ b/src/Rules/Functions/RandomIntParametersRule.php @@ -0,0 +1,84 @@ + + */ +class RandomIntParametersRule implements \PHPStan\Rules\Rule +{ + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** @var bool */ + private $reportMaybes; + + public function __construct(ReflectionProvider $reflectionProvider, bool $reportMaybes) + { + $this->reflectionProvider = $reflectionProvider; + $this->reportMaybes = $reportMaybes; + } + + public function getNodeType(): string + { + return FuncCall::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (!($node->name instanceof \PhpParser\Node\Name)) { + return []; + } + + if ($this->reflectionProvider->resolveFunctionName($node->name, $scope) !== 'random_int') { + return []; + } + + $minType = $scope->getType($node->args[0]->value)->toInteger(); + $maxType = $scope->getType($node->args[1]->value)->toInteger(); + $integerType = new IntegerType(); + + if ($minType->equals($integerType) || $maxType->equals($integerType)) { + return []; + } + + if ($minType instanceof ConstantIntegerType || $minType instanceof IntegerRangeType) { + if ($minType instanceof ConstantIntegerType) { + $maxPermittedType = IntegerRangeType::fromInterval($minType->getValue(), PHP_INT_MAX); + } else { + $maxPermittedType = IntegerRangeType::fromInterval($minType->getMax(), PHP_INT_MAX); + } + + if (!$maxPermittedType->isSuperTypeOf($maxType)->yes()) { + $message = 'Parameter #1 $min (%s) of function random_int expects lower number than parameter #2 $max (%s).'; + + // True if sometimes the parameters conflict. + $isMaybe = !$maxType->isSuperTypeOf($minType)->no(); + + if (!$isMaybe || $this->reportMaybes) { + return [ + RuleErrorBuilder::message(sprintf( + $message, + $minType->describe(VerbosityLevel::value()), + $maxType->describe(VerbosityLevel::value()) + ))->build(), + ]; + } + } + } + + return []; + } + +} diff --git a/src/Type/Php/RandomIntFunctionReturnTypeExtension.php b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php new file mode 100644 index 0000000000..30e9c0f89d --- /dev/null +++ b/src/Type/Php/RandomIntFunctionReturnTypeExtension.php @@ -0,0 +1,63 @@ +getName() === 'random_int'; + } + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + if (count($functionCall->args) < 2) { + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + $minType = $scope->getType($functionCall->args[0]->value)->toInteger(); + $maxType = $scope->getType($functionCall->args[1]->value)->toInteger(); + + return $this->createRange($minType, $maxType); + } + + private function createRange(Type $minType, Type $maxType): Type + { + $minValue = array_reduce($minType instanceof UnionType ? $minType->getTypes() : [$minType], static function (int $carry, Type $type): int { + if ($type instanceof IntegerRangeType) { + $value = $type->getMin(); + } elseif ($type instanceof ConstantIntegerType) { + $value = $type->getValue(); + } else { + $value = PHP_INT_MIN; + } + + return min($value, $carry); + }, PHP_INT_MAX); + + $maxValue = array_reduce($maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], static function (int $carry, Type $type): int { + if ($type instanceof IntegerRangeType) { + $value = $type->getMax(); + } elseif ($type instanceof ConstantIntegerType) { + $value = $type->getValue(); + } else { + $value = PHP_INT_MAX; + } + + return max($value, $carry); + }, PHP_INT_MIN); + + return IntegerRangeType::fromInterval($minValue, $maxValue); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0b530fd57c..b2515354d8 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -9672,6 +9672,11 @@ public function dataIntegerRangeTypes(): array return $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php'); } + public function dataRandomInt(): array + { + return $this->gatherAssertTypes(__DIR__ . '/data/random-int.php'); + } + public function dataClosureReturnTypes(): array { return $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php'); @@ -9774,6 +9779,7 @@ public function dataBug2750(): array * @dataProvider dataGenericClassStringType * @dataProvider dataInstanceOf * @dataProvider dataIntegerRangeTypes + * @dataProvider dataRandomInt * @dataProvider dataClosureReturnTypes * @dataProvider dataArrayKey * @dataProvider dataIntersectionStatic diff --git a/tests/PHPStan/Analyser/data/random-int.php b/tests/PHPStan/Analyser/data/random-int.php new file mode 100644 index 0000000000..115bcc240f --- /dev/null +++ b/tests/PHPStan/Analyser/data/random-int.php @@ -0,0 +1,39 @@ +', random_int($min, 20)); +}; + +function (int $min) { + \assert($min <= 0); + assertType('int', random_int($min, 20)); +}; + +function (int $max) { + \assert($min >= 0); + assertType('int<0, max>', random_int(0, $max)); +}; + +function (int $i) { + assertType('int', random_int($i, $i)); +}; + +assertType('0', random_int(0, 0)); +assertType('int', random_int(PHP_INT_MIN, PHP_INT_MAX)); +assertType('int<0, max>', random_int(0, PHP_INT_MAX)); +assertType('int', random_int(PHP_INT_MIN, 0)); +assertType('int<-1, 1>', random_int(-1, 1)); +assertType('int<0, 30>', random_int(0, random_int(0, 30))); +assertType('int<0, 100>', random_int(random_int(0, 10), 100)); + +assertType('*NEVER*', random_int(10, 1)); +assertType('*NEVER*', random_int(2, random_int(0, 1))); +assertType('int<0, 1>', random_int(0, random_int(0, 1))); +assertType('*NEVER*', random_int(random_int(0, 1), -1)); +assertType('int<0, 1>', random_int(random_int(0, 1), 1)); + +assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5))); +assertType('int', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX))); diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index e8c9f7a44a..03182cd541 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -183,5 +183,20 @@ "message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('bar' => 'date') given.", "line": 576, "ignorable": true + }, + { + "message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).", + "line": 662, + "ignorable": true + }, + { + "message": "Parameter #1 $min (int<11, max>) of function random_int expects lower number than parameter #2 $max (10).", + "line": 669, + "ignorable": true + }, + { + "message": "Parameter #1 $min (340) of function random_int expects lower number than parameter #2 $max (int).", + "line": 676, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes-7.json b/tests/PHPStan/Levels/data/acceptTypes-7.json index 44c08b3082..f073109688 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-7.json +++ b/tests/PHPStan/Levels/data/acceptTypes-7.json @@ -113,5 +113,20 @@ "message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.", "line": 639, "ignorable": true + }, + { + "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).", + "line": 681, + "ignorable": true + }, + { + "message": "Parameter #1 $min (int<-1, 0>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", + "line": 682, + "ignorable": true + }, + { + "message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).", + "line": 683, + "ignorable": true } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index 537fa37c50..a2af793b47 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -653,3 +653,34 @@ public function requireStatic($static): void } } + +class RandomInt +{ + + public function doThings(): int + { + return random_int(0, -1); + } + + public function doInputMin(int $input): int + { + assert($input > 10); + + return random_int($input, 10); + } + + public function doInputMax(int $input): int + { + assert($input < 340); + + return random_int(340, $input); + } + + public function doStuff(): void + { + random_int(random_int(-1, 1), random_int(0, 1)); + random_int(random_int(-1, 0), random_int(-1, 1)); + random_int(random_int(-1, 1), random_int(-1, 1)); + } + +} diff --git a/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php new file mode 100644 index 0000000000..06d0f8a03a --- /dev/null +++ b/tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php @@ -0,0 +1,58 @@ + + */ +class RandomIntParametersRuleTest extends \PHPStan\Testing\RuleTestCase +{ + + protected function getRule(): \PHPStan\Rules\Rule + { + return new RandomIntParametersRule($this->createReflectionProvider(), true); + } + + public function testFile(): void + { + $this->analyse([__DIR__ . '/data/random-int.php'], [ + [ + 'Parameter #1 $min (1) of function random_int expects lower number than parameter #2 $max (0).', + 8, + ], + [ + 'Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).', + 9, + ], + [ + 'Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (int<-10, -1>).', + 11, + ], + [ + 'Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (int<-10, 10>).', + 12, + ], + [ + 'Parameter #1 $min (int<1, 10>) of function random_int expects lower number than parameter #2 $max (0).', + 15, + ], + [ + 'Parameter #1 $min (int<-10, 10>) of function random_int expects lower number than parameter #2 $max (0).', + 16, + ], + [ + 'Parameter #1 $min (int<-5, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 5>).', + 19, + ], + [ + 'Parameter #1 $min (int<-5, 0>) of function random_int expects lower number than parameter #2 $max (int<-1, 5>).', + 20, + ], + [ + 'Parameter #1 $min (int<0, 10>) of function random_int expects lower number than parameter #2 $max (int<0, 10>).', + 31, + ], + ]); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/random-int.php b/tests/PHPStan/Rules/Functions/data/random-int.php new file mode 100644 index 0000000000..2b2482410b --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/random-int.php @@ -0,0 +1,33 @@ +