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

}