Skip to content

Commit 7e57ca0

Browse files
cs278ondrejmirtes
authored andcommitted
random_int() return type and parameters rule
1 parent 4724469 commit 7e57ca0

12 files changed

Lines changed: 364 additions & 0 deletions

conf/bleedingEdge.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
parameters:
2+
featureToggles:
3+
randomIntParameters: true

conf/config.level5.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
includes:
22
- config.level4.neon
33

4+
conditionalTags:
5+
PHPStan\Rules\Functions\RandomIntParametersRule:
6+
phpstan.rules.rule: %featureToggles.randomIntParameters%
7+
48
parameters:
59
checkFunctionArgumentTypes: true
610
checkArgumentsPassedByReference: true
11+
featureToggles:
12+
randomIntParameters: false
13+
14+
services:
15+
-
16+
class: PHPStan\Rules\Functions\RandomIntParametersRule
17+
arguments:
18+
reportMaybes: %reportMaybes%

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,11 @@ services:
717717
tags:
718718
- phpstan.broker.dynamicFunctionReturnTypeExtension
719719

720+
-
721+
class: PHPStan\Type\Php\RandomIntFunctionReturnTypeExtension
722+
tags:
723+
- phpstan.broker.dynamicFunctionReturnTypeExtension
724+
720725
-
721726
class: PHPStan\Type\Php\RangeFunctionReturnTypeExtension
722727
tags:
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\Functions;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\FuncCall;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\ReflectionProvider;
9+
use PHPStan\Rules\RuleErrorBuilder;
10+
use PHPStan\Type\Constant\ConstantIntegerType;
11+
use PHPStan\Type\IntegerRangeType;
12+
use PHPStan\Type\IntegerType;
13+
use PHPStan\Type\VerbosityLevel;
14+
15+
/**
16+
* @implements \PHPStan\Rules\Rule<\PhpParser\Node\Expr\FuncCall>
17+
*/
18+
class RandomIntParametersRule implements \PHPStan\Rules\Rule
19+
{
20+
21+
/** @var ReflectionProvider */
22+
private $reflectionProvider;
23+
24+
/** @var bool */
25+
private $reportMaybes;
26+
27+
public function __construct(ReflectionProvider $reflectionProvider, bool $reportMaybes)
28+
{
29+
$this->reflectionProvider = $reflectionProvider;
30+
$this->reportMaybes = $reportMaybes;
31+
}
32+
33+
public function getNodeType(): string
34+
{
35+
return FuncCall::class;
36+
}
37+
38+
public function processNode(Node $node, Scope $scope): array
39+
{
40+
if (!($node->name instanceof \PhpParser\Node\Name)) {
41+
return [];
42+
}
43+
44+
if ($this->reflectionProvider->resolveFunctionName($node->name, $scope) !== 'random_int') {
45+
return [];
46+
}
47+
48+
$minType = $scope->getType($node->args[0]->value)->toInteger();
49+
$maxType = $scope->getType($node->args[1]->value)->toInteger();
50+
$integerType = new IntegerType();
51+
52+
if ($minType->equals($integerType) || $maxType->equals($integerType)) {
53+
return [];
54+
}
55+
56+
if ($minType instanceof ConstantIntegerType || $minType instanceof IntegerRangeType) {
57+
if ($minType instanceof ConstantIntegerType) {
58+
$maxPermittedType = IntegerRangeType::fromInterval($minType->getValue(), PHP_INT_MAX);
59+
} else {
60+
$maxPermittedType = IntegerRangeType::fromInterval($minType->getMax(), PHP_INT_MAX);
61+
}
62+
63+
if (!$maxPermittedType->isSuperTypeOf($maxType)->yes()) {
64+
$message = 'Parameter #1 $min (%s) of function random_int expects lower number than parameter #2 $max (%s).';
65+
66+
// True if sometimes the parameters conflict.
67+
$isMaybe = !$maxType->isSuperTypeOf($minType)->no();
68+
69+
if (!$isMaybe || $this->reportMaybes) {
70+
return [
71+
RuleErrorBuilder::message(sprintf(
72+
$message,
73+
$minType->describe(VerbosityLevel::value()),
74+
$maxType->describe(VerbosityLevel::value())
75+
))->build(),
76+
];
77+
}
78+
}
79+
}
80+
81+
return [];
82+
}
83+
84+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Reflection\ParametersAcceptorSelector;
9+
use PHPStan\Type\Constant\ConstantIntegerType;
10+
use PHPStan\Type\IntegerRangeType;
11+
use PHPStan\Type\Type;
12+
use PHPStan\Type\UnionType;
13+
14+
class RandomIntFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
15+
{
16+
17+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
18+
{
19+
return $functionReflection->getName() === 'random_int';
20+
}
21+
22+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
23+
{
24+
if (count($functionCall->args) < 2) {
25+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
26+
}
27+
28+
$minType = $scope->getType($functionCall->args[0]->value)->toInteger();
29+
$maxType = $scope->getType($functionCall->args[1]->value)->toInteger();
30+
31+
return $this->createRange($minType, $maxType);
32+
}
33+
34+
private function createRange(Type $minType, Type $maxType): Type
35+
{
36+
$minValue = array_reduce($minType instanceof UnionType ? $minType->getTypes() : [$minType], static function (int $carry, Type $type): int {
37+
if ($type instanceof IntegerRangeType) {
38+
$value = $type->getMin();
39+
} elseif ($type instanceof ConstantIntegerType) {
40+
$value = $type->getValue();
41+
} else {
42+
$value = PHP_INT_MIN;
43+
}
44+
45+
return min($value, $carry);
46+
}, PHP_INT_MAX);
47+
48+
$maxValue = array_reduce($maxType instanceof UnionType ? $maxType->getTypes() : [$maxType], static function (int $carry, Type $type): int {
49+
if ($type instanceof IntegerRangeType) {
50+
$value = $type->getMax();
51+
} elseif ($type instanceof ConstantIntegerType) {
52+
$value = $type->getValue();
53+
} else {
54+
$value = PHP_INT_MAX;
55+
}
56+
57+
return max($value, $carry);
58+
}, PHP_INT_MIN);
59+
60+
return IntegerRangeType::fromInterval($minValue, $maxValue);
61+
}
62+
63+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9672,6 +9672,11 @@ public function dataIntegerRangeTypes(): array
96729672
return $this->gatherAssertTypes(__DIR__ . '/data/integer-range-types.php');
96739673
}
96749674

9675+
public function dataRandomInt(): array
9676+
{
9677+
return $this->gatherAssertTypes(__DIR__ . '/data/random-int.php');
9678+
}
9679+
96759680
public function dataClosureReturnTypes(): array
96769681
{
96779682
return $this->gatherAssertTypes(__DIR__ . '/data/closure-return-type-extensions.php');
@@ -9774,6 +9779,7 @@ public function dataBug2750(): array
97749779
* @dataProvider dataGenericClassStringType
97759780
* @dataProvider dataInstanceOf
97769781
* @dataProvider dataIntegerRangeTypes
9782+
* @dataProvider dataRandomInt
97779783
* @dataProvider dataClosureReturnTypes
97789784
* @dataProvider dataArrayKey
97799785
* @dataProvider dataIntersectionStatic
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
use function PHPStan\Analyser\assertType;
4+
5+
function (int $min) {
6+
\assert($min === 10 || $min === 15);
7+
assertType('int<10, 20>', random_int($min, 20));
8+
};
9+
10+
function (int $min) {
11+
\assert($min <= 0);
12+
assertType('int<min, 20>', random_int($min, 20));
13+
};
14+
15+
function (int $max) {
16+
\assert($min >= 0);
17+
assertType('int<0, max>', random_int(0, $max));
18+
};
19+
20+
function (int $i) {
21+
assertType('int', random_int($i, $i));
22+
};
23+
24+
assertType('0', random_int(0, 0));
25+
assertType('int', random_int(PHP_INT_MIN, PHP_INT_MAX));
26+
assertType('int<0, max>', random_int(0, PHP_INT_MAX));
27+
assertType('int<min, 0>', random_int(PHP_INT_MIN, 0));
28+
assertType('int<-1, 1>', random_int(-1, 1));
29+
assertType('int<0, 30>', random_int(0, random_int(0, 30)));
30+
assertType('int<0, 100>', random_int(random_int(0, 10), 100));
31+
32+
assertType('*NEVER*', random_int(10, 1));
33+
assertType('*NEVER*', random_int(2, random_int(0, 1)));
34+
assertType('int<0, 1>', random_int(0, random_int(0, 1)));
35+
assertType('*NEVER*', random_int(random_int(0, 1), -1));
36+
assertType('int<0, 1>', random_int(random_int(0, 1), 1));
37+
38+
assertType('int<-5, 5>', random_int(random_int(-5, 0), random_int(0, 5)));
39+
assertType('int', random_int(random_int(PHP_INT_MIN, 0), random_int(0, PHP_INT_MAX)));

tests/PHPStan/Levels/data/acceptTypes-5.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,5 +183,20 @@
183183
"message": "Parameter #1 $one of method Levels\\AcceptTypes\\ArrayShapes::doBar() expects array('foo' => callable(): mixed), array('bar' => 'date') given.",
184184
"line": 576,
185185
"ignorable": true
186+
},
187+
{
188+
"message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).",
189+
"line": 662,
190+
"ignorable": true
191+
},
192+
{
193+
"message": "Parameter #1 $min (int<11, max>) of function random_int expects lower number than parameter #2 $max (10).",
194+
"line": 669,
195+
"ignorable": true
196+
},
197+
{
198+
"message": "Parameter #1 $min (340) of function random_int expects lower number than parameter #2 $max (int<min, 339>).",
199+
"line": 676,
200+
"ignorable": true
186201
}
187202
]

tests/PHPStan/Levels/data/acceptTypes-7.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,5 +113,20 @@
113113
"message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.",
114114
"line": 639,
115115
"ignorable": true
116+
},
117+
{
118+
"message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).",
119+
"line": 681,
120+
"ignorable": true
121+
},
122+
{
123+
"message": "Parameter #1 $min (int<-1, 0>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).",
124+
"line": 682,
125+
"ignorable": true
126+
},
127+
{
128+
"message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<-1, 1>).",
129+
"line": 683,
130+
"ignorable": true
116131
}
117132
]

tests/PHPStan/Levels/data/acceptTypes.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,3 +653,34 @@ public function requireStatic($static): void
653653
}
654654

655655
}
656+
657+
class RandomInt
658+
{
659+
660+
public function doThings(): int
661+
{
662+
return random_int(0, -1);
663+
}
664+
665+
public function doInputMin(int $input): int
666+
{
667+
assert($input > 10);
668+
669+
return random_int($input, 10);
670+
}
671+
672+
public function doInputMax(int $input): int
673+
{
674+
assert($input < 340);
675+
676+
return random_int(340, $input);
677+
}
678+
679+
public function doStuff(): void
680+
{
681+
random_int(random_int(-1, 1), random_int(0, 1));
682+
random_int(random_int(-1, 0), random_int(-1, 1));
683+
random_int(random_int(-1, 1), random_int(-1, 1));
684+
}
685+
686+
}

0 commit comments

Comments
 (0)