Skip to content

Commit 40ece9a

Browse files
pepakrizondrejmirtes
authored andcommitted
Return type extension for range() function
1 parent 374ec41 commit 40ece9a

File tree

4 files changed

+194
-0
lines changed

4 files changed

+194
-0
lines changed

conf/config.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ services:
296296
tags:
297297
- phpstan.broker.dynamicFunctionReturnTypeExtension
298298

299+
-
300+
class: PHPStan\Type\Php\RangeFunctionReturnTypeExtension
301+
tags:
302+
- phpstan.broker.dynamicFunctionReturnTypeExtension
303+
299304
-
300305
class: PHPStan\Type\Php\AssertFunctionTypeSpecifyingExtension
301306
tags:
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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\ArrayType;
10+
use PHPStan\Type\Constant\ConstantArrayType;
11+
use PHPStan\Type\Constant\ConstantFloatType;
12+
use PHPStan\Type\Constant\ConstantIntegerType;
13+
use PHPStan\Type\FloatType;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\Type;
16+
use PHPStan\Type\TypeCombinator;
17+
use PHPStan\Type\TypeUtils;
18+
use PHPStan\Type\UnionType;
19+
20+
class RangeFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
21+
{
22+
23+
private const RANGE_LENGTH_THRESHOLD = 50;
24+
25+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
26+
{
27+
return $functionReflection->getName() === 'range';
28+
}
29+
30+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
31+
{
32+
if (count($functionCall->args) < 2) {
33+
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
34+
}
35+
36+
$startType = $scope->getType($functionCall->args[0]->value);
37+
$endType = $scope->getType($functionCall->args[1]->value);
38+
$stepType = count($functionCall->args) >= 3 ? $scope->getType($functionCall->args[2]->value) : new ConstantIntegerType(1);
39+
40+
$constantReturnTypes = [];
41+
42+
$startConstants = TypeUtils::getConstantScalars($startType);
43+
foreach ($startConstants as $startConstant) {
44+
if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType) {
45+
continue;
46+
}
47+
48+
$endConstants = TypeUtils::getConstantScalars($endType);
49+
foreach ($endConstants as $endConstant) {
50+
if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType) {
51+
continue;
52+
}
53+
54+
$stepConstants = TypeUtils::getConstantScalars($stepType);
55+
foreach ($stepConstants as $stepConstant) {
56+
if (!$stepConstant instanceof ConstantIntegerType && !$stepConstant instanceof ConstantFloatType) {
57+
continue;
58+
}
59+
60+
$rangeLength = (int) ceil(abs($startConstant->getValue() - $endConstant->getValue()) / $stepConstant->getValue()) + 1;
61+
if ($rangeLength > self::RANGE_LENGTH_THRESHOLD) {
62+
continue;
63+
}
64+
65+
$keyTypes = [];
66+
$valueTypes = [];
67+
68+
$rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue());
69+
foreach ($rangeValues as $key => $value) {
70+
$keyTypes[] = new ConstantIntegerType($key);
71+
$valueTypes[] = $scope->getTypeFromValue($value);
72+
}
73+
74+
$constantReturnTypes[] = new ConstantArrayType($keyTypes, $valueTypes, $rangeLength);
75+
}
76+
}
77+
}
78+
79+
if (count($constantReturnTypes) > 0) {
80+
return TypeCombinator::union(...$constantReturnTypes);
81+
}
82+
83+
$startType = TypeUtils::generalizeType($startType);
84+
$endType = TypeUtils::generalizeType($endType);
85+
$stepType = TypeUtils::generalizeType($stepType);
86+
87+
if (
88+
$startType instanceof IntegerType
89+
&& $endType instanceof IntegerType
90+
&& $stepType instanceof IntegerType
91+
) {
92+
return new ArrayType(new IntegerType(), new IntegerType());
93+
}
94+
95+
if (
96+
$startType instanceof FloatType
97+
|| $endType instanceof FloatType
98+
|| $stepType instanceof FloatType
99+
) {
100+
return new ArrayType(new IntegerType(), new FloatType());
101+
}
102+
103+
return new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new FloatType()]));
104+
}
105+
106+
}

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4189,6 +4189,77 @@ public function testFunctions(
41894189
);
41904190
}
41914191

4192+
public function dataRangeFunction(): array
4193+
{
4194+
return [
4195+
[
4196+
'array(2, 3, 4, 5)',
4197+
'range(2, 5)',
4198+
],
4199+
[
4200+
'array(2, 4)',
4201+
'range(2, 5, 2)',
4202+
],
4203+
[
4204+
'array(2.0, 3.0, 4.0, 5.0)',
4205+
'range(2, 5, 1.0)',
4206+
],
4207+
[
4208+
'array(2.1, 3.1, 4.1)',
4209+
'range(2.1, 5)',
4210+
],
4211+
[
4212+
'array<int, int>',
4213+
'range(2, 5, $integer)',
4214+
],
4215+
[
4216+
'array<int, float>',
4217+
'range($float, 5, $integer)',
4218+
],
4219+
[
4220+
'array<int, float>',
4221+
'range($float, $mixed, $integer)',
4222+
],
4223+
[
4224+
'array<int, float|int>',
4225+
'range($integer, $mixed)',
4226+
],
4227+
[
4228+
'array(0 => 1, ?1 => 2)',
4229+
'range(1, doFoo() ? 1 : 2)',
4230+
],
4231+
[
4232+
'array(0 => -1|1, ?1 => 0|2, ?2 => 1, ?3 => 2)',
4233+
'range(doFoo() ? -1 : 1, doFoo() ? 1 : 2)',
4234+
],
4235+
[
4236+
'array(3, 2, 1, 0, -1)',
4237+
'range(3, -1)',
4238+
],
4239+
[
4240+
'array<int, int>',
4241+
'range(0, 50)',
4242+
],
4243+
];
4244+
}
4245+
4246+
/**
4247+
* @dataProvider dataRangeFunction
4248+
* @param string $description
4249+
* @param string $expression
4250+
*/
4251+
public function testRangeFunction(
4252+
string $description,
4253+
string $expression
4254+
): void
4255+
{
4256+
$this->assertTypes(
4257+
__DIR__ . '/data/range-function.php',
4258+
$description,
4259+
$expression
4260+
);
4261+
}
4262+
41924263
public function dataSpecifiedTypesUsingIsFunctions(): array
41934264
{
41944265
return [
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
/** @var int $integer */
4+
$integer = doFoo();
5+
6+
/** @var float $float */
7+
$float = doFoo();
8+
9+
/** @var mixed $mixed */
10+
$mixed = doFoo();
11+
12+
die;

0 commit comments

Comments
 (0)