Skip to content

Commit

Permalink
random_int() return type and parameters rule
Browse files Browse the repository at this point in the history
  • Loading branch information
cs278 authored and ondrejmirtes committed Jan 16, 2020
1 parent 4724469 commit 7e57ca0
Show file tree
Hide file tree
Showing 12 changed files with 364 additions and 0 deletions.
3 changes: 3 additions & 0 deletions conf/bleedingEdge.neon
@@ -0,0 +1,3 @@
parameters:
featureToggles:
randomIntParameters: true
12 changes: 12 additions & 0 deletions 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%
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
84 changes: 84 additions & 0 deletions src/Rules/Functions/RandomIntParametersRule.php
@@ -0,0 +1,84 @@
<?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;

/** @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 [];
}

}
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 @@ -9774,6 +9779,7 @@ public function dataBug2750(): 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)));
15 changes: 15 additions & 0 deletions tests/PHPStan/Levels/data/acceptTypes-5.json
Expand Up @@ -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<min, 339>).",
"line": 676,
"ignorable": true
}
]
15 changes: 15 additions & 0 deletions tests/PHPStan/Levels/data/acceptTypes-7.json
Expand Up @@ -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
}
]
31 changes: 31 additions & 0 deletions tests/PHPStan/Levels/data/acceptTypes.php
Expand Up @@ -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));
}

}
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(), 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,
],
]);
}

}

0 comments on commit 7e57ca0

Please sign in to comment.