Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 7e57ca0

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

File tree

12 files changed

+364
-0
lines changed

12 files changed

+364
-0
lines changed

‎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)

AltStyle によって変換されたページ (->オリジナル) /