Skip to content

Commit

Permalink
Fix range() with string arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Nov 7, 2020
1 parent d3be969 commit 67a905a
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 34 deletions.
2 changes: 1 addition & 1 deletion resources/functionMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -9065,7 +9065,7 @@
'rand\'1' => ['int'],
'random_bytes' => ['string', 'length'=>'int'],
'random_int' => ['int', 'min'=>'int', 'max'=>'int'],
'range' => ['array', 'low'=>'mixed', 'high'=>'mixed', 'step='=>'int|float'],
'range' => ['array', 'low'=>'int|float|string', 'high'=>'int|float|string', 'step='=>'int|float'],
'RangeException::__clone' => ['void'],
'RangeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'(?Throwable)|(?RangeException)'],
'RangeException::__toString' => ['string'],
Expand Down
3 changes: 2 additions & 1 deletion src/Type/Constant/ConstantArrayTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use function array_filter;

class ConstantArrayTypeBuilder
Expand Down Expand Up @@ -97,7 +98,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
return;
}

$this->keyTypes[] = $offsetType;
$this->keyTypes[] = TypeUtils::generalizeType($offsetType);
$this->valueTypes[] = $valueType;
$this->degradeToGeneralArray = true;
}
Expand Down
60 changes: 31 additions & 29 deletions src/Type/Php/RangeFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
Expand Down Expand Up @@ -41,13 +44,13 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,

$startConstants = TypeUtils::getConstantScalars($startType);
foreach ($startConstants as $startConstant) {
if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType) {
if (!$startConstant instanceof ConstantIntegerType && !$startConstant instanceof ConstantFloatType && !$startConstant instanceof ConstantStringType) {
continue;
}

$endConstants = TypeUtils::getConstantScalars($endType);
foreach ($endConstants as $endConstant) {
if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType) {
if (!$endConstant instanceof ConstantIntegerType && !$endConstant instanceof ConstantFloatType && !$endConstant instanceof ConstantStringType) {
continue;
}

Expand All @@ -57,21 +60,16 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
continue;
}

$rangeLength = (int) ceil(abs($startConstant->getValue() - $endConstant->getValue()) / $stepConstant->getValue()) + 1;
if ($rangeLength > self::RANGE_LENGTH_THRESHOLD) {
continue;
}

$keyTypes = [];
$valueTypes = [];

$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$rangeValues = range($startConstant->getValue(), $endConstant->getValue(), $stepConstant->getValue());
foreach ($rangeValues as $key => $value) {
$keyTypes[] = new ConstantIntegerType($key);
$valueTypes[] = $scope->getTypeFromValue($value);
if (count($rangeValues) > self::RANGE_LENGTH_THRESHOLD) {
$arrayBuilder->degradeToGeneralArray();
}
foreach ($rangeValues as $value) {
$arrayBuilder->setOffsetValueType(null, $scope->getTypeFromValue($value));
}

$constantReturnTypes[] = new ConstantArrayType($keyTypes, $valueTypes, $rangeLength);
$constantReturnTypes[] = $arrayBuilder->getArray();
}
}
}
Expand All @@ -80,27 +78,31 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return TypeCombinator::union(...$constantReturnTypes);
}

$startType = TypeUtils::generalizeType($startType);
$endType = TypeUtils::generalizeType($endType);
$stepType = TypeUtils::generalizeType($stepType);
$argType = TypeCombinator::union($startType, $endType);
$isInteger = (new IntegerType())->isSuperTypeOf($argType)->yes();
$isStepInteger = (new IntegerType())->isSuperTypeOf($stepType)->yes();

if (
$startType instanceof IntegerType
&& $endType instanceof IntegerType
&& $stepType instanceof IntegerType
) {
if ($isInteger && $isStepInteger) {
return new ArrayType(new IntegerType(), new IntegerType());
}

if (
$startType instanceof FloatType
|| $endType instanceof FloatType
|| $stepType instanceof FloatType
) {
$isFloat = (new FloatType())->isSuperTypeOf($argType)->yes();
if ($isFloat) {
return new ArrayType(new IntegerType(), new FloatType());
}

return new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new FloatType()]));
$numberType = new UnionType([new IntegerType(), new FloatType()]);
$isNumber = $numberType->isSuperTypeOf($argType)->yes();
if ($isNumber) {
return new ArrayType(new IntegerType(), $numberType);
}

$isString = (new StringType())->isSuperTypeOf($argType)->yes();
if ($isString) {
return new ArrayType(new IntegerType(), new StringType());
}

return new ArrayType(new IntegerType(), new BenevolentUnionType([new IntegerType(), new FloatType(), new StringType()]));
}

}
12 changes: 9 additions & 3 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6076,15 +6076,15 @@ public function dataRangeFunction(): array
'range(2, 5, $integer)',
],
[
'array<int, float>',
'array<int, float|int>',
'range($float, 5, $integer)',
],
[
'array<int, float>',
'array<int, (float|int|string)>',
'range($float, $mixed, $integer)',
],
[
'array<int, float|int>',
'array<int, (float|int|string)>',
'range($integer, $mixed)',
],
[
Expand Down Expand Up @@ -10279,6 +10279,11 @@ public function dataBug3915(): array
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3915.php');
}

public function dataBug2378(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2378.php');
}

/**
* @param string $file
* @return array<string, mixed[]>
Expand Down Expand Up @@ -10449,6 +10454,7 @@ private function gatherAssertTypes(string $file): array
* @dataProvider dataPromotedProperties
* @dataProvider dataNeverEarlyTerminates
* @dataProvider dataBug3915
* @dataProvider dataBug2378
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
23 changes: 23 additions & 0 deletions tests/PHPStan/Analyser/data/bug-2378.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Bug2375;

use function PHPStan\Analyser\assertType;

class Foo
{

public function doFoo(
$mixed,
int $int,
string $s,
float $f
): void
{
assertType('array(\'a\', \'b\', \'c\', \'d\')', range('a', 'd'));
assertType('array(\'a\', \'c\', \'e\', \'g\', \'i\')', range('a', 'i', 2));

assertType('array<int, string>', range($s, $s));
}

}

0 comments on commit 67a905a

Please sign in to comment.