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

random_int() return type and parameters rule #99

Merged
merged 9 commits into from Jan 16, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/bleedingEdge.neon
@@ -0,0 +1,3 @@
parameters:
featureToggles:
randomIntParameters: true
10 changes: 10 additions & 0 deletions conf/config.level5.neon
@@ -1,6 +1,16 @@
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
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -717,6 +717,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

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

-
class: PHPStan\Type\Php\RangeFunctionReturnTypeExtension
tags:
Expand Down
78 changes: 78 additions & 0 deletions src/Rules/Functions/RandomIntParametersRule.php
@@ -0,0 +1,78 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

use PhpParser\Node;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\VerbosityLevel;

/**
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall>
*/
class RandomIntParametersRule implements \PHPStan\Rules\Rule
{

/** @var ReflectionProvider */
private $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

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();

if ($minType->equals(new IntegerType()) || $maxType->equals(new IntegerType())) {
cs278 marked this conversation as resolved.
Show resolved Hide resolved
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 = 'Cannot call random_int() when $min parameter (%s) can be greater than $max parameter (%s).';

if ($maxType->isSuperTypeOf($minType)->no()) {
$message = 'Cannot call random_int() when $min parameter (%s) is greater than $max parameter (%s).';
cs278 marked this conversation as resolved.
Show resolved Hide resolved
}

return [
RuleErrorBuilder::message(sprintf(
$message,
$minType->describe(VerbosityLevel::value()),
$maxType->describe(VerbosityLevel::value())
))->build(),
];
}
}

return [];
}

}
63 changes: 63 additions & 0 deletions src/Type/Php/RandomIntFunctionReturnTypeExtension.php
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

class RandomIntFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->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);
}

}
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -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');
Expand Down Expand Up @@ -9769,6 +9774,7 @@ public function dataBug2443(): array
* @dataProvider dataGenericClassStringType
* @dataProvider dataInstanceOf
* @dataProvider dataIntegerRangeTypes
* @dataProvider dataRandomInt
* @dataProvider dataClosureReturnTypes
* @dataProvider dataArrayKey
* @dataProvider dataIntersectionStatic
Expand Down
39 changes: 39 additions & 0 deletions tests/PHPStan/Analyser/data/random-int.php
@@ -0,0 +1,39 @@
<?php

use function PHPStan\Analyser\assertType;

function (int $min) {
\assert($min === 10 || $min === 15);
assertType('int<10, 20>', random_int($min, 20));
};

function (int $min) {
\assert($min <= 0);
assertType('int<min, 20>', 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<min, 0>', 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)));
58 changes: 58 additions & 0 deletions tests/PHPStan/Rules/Functions/RandomIntParametersRuleTest.php
@@ -0,0 +1,58 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Functions;

/**
* @extends \PHPStan\Testing\RuleTestCase<RandomIntParametersRule>
*/
class RandomIntParametersRuleTest extends \PHPStan\Testing\RuleTestCase
{

protected function getRule(): \PHPStan\Rules\Rule
{
return new RandomIntParametersRule($this->createReflectionProvider());
}

public function testFile(): void
{
$this->analyse([__DIR__ . '/data/random-int.php'], [
[
'Cannot call random_int() when $min parameter (1) is greater than $max parameter (0).',
8,
],
[
'Cannot call random_int() when $min parameter (0) is greater than $max parameter (-1).',
9,
],
[
'Cannot call random_int() when $min parameter (0) is greater than $max parameter (int<-10, -1>).',
11,
],
[
'Cannot call random_int() when $min parameter (0) can be greater than $max parameter (int<-10, 10>).',
12,
],
[
'Cannot call random_int() when $min parameter (int<1, 10>) is greater than $max parameter (0).',
15,
],
[
'Cannot call random_int() when $min parameter (int<-10, 10>) can be greater than $max parameter (0).',
16,
],
[
'Cannot call random_int() when $min parameter (int<-5, 1>) can be greater than $max parameter (int<0, 5>).',
19,
],
[
'Cannot call random_int() when $min parameter (int<-5, 0>) can be greater than $max parameter (int<-1, 5>).',
20,
],
[
'Cannot call random_int() when $min parameter (int<0, 10>) can be greater than $max parameter (int<0, 10>).',
31,
],
]);
}

}
33 changes: 33 additions & 0 deletions tests/PHPStan/Rules/Functions/data/random-int.php
@@ -0,0 +1,33 @@
<?php

random_int(0, 0);
random_int(0, 1);
random_int(-1, 0);
random_int(-1, 1);

random_int(1, 0);
random_int(0, -1);

random_int(0, random_int(-10, -1));
random_int(0, random_int(-10, 10));
random_int(0, random_int(0, 10)); // ok

random_int(random_int(1, 10), 0);
random_int(random_int(-10, 10), 0);
random_int(random_int(-10, 0), 0); // ok

random_int(random_int(-5, 1), random_int(0, 5));
random_int(random_int(-5, 0), random_int(-1, 5));

/** @var int */
$x = foo();
/** @var int */
$y = bar();

random_int($x, $y);
random_int(0, $x);
random_int($x, random_int(0, PHP_INT_MAX));
random_int(random_int(PHP_INT_MIN, 0), $x);
random_int(random_int(0, 10), random_int(0, 10)); // Equal args are okay except ranges.

random_int(PHP_INT_MAX, PHP_INT_MIN); // @todo this should error