diff --git a/resources/functionMap.php b/resources/functionMap.php index b157cf3da1..e95ce97992 100644 --- a/resources/functionMap.php +++ b/resources/functionMap.php @@ -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'], diff --git a/src/Type/Constant/ConstantArrayTypeBuilder.php b/src/Type/Constant/ConstantArrayTypeBuilder.php index 84fce75847..b5260b4164 100644 --- a/src/Type/Constant/ConstantArrayTypeBuilder.php +++ b/src/Type/Constant/ConstantArrayTypeBuilder.php @@ -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 @@ -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; } diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 65a7ab3236..91f0bc108f 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -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; @@ -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; } @@ -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(); } } } @@ -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()])); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 0d257771b3..6a322d4a4c 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -6076,15 +6076,15 @@ public function dataRangeFunction(): array 'range(2, 5, $integer)', ], [ - 'array', + 'array', 'range($float, 5, $integer)', ], [ - 'array', + 'array', 'range($float, $mixed, $integer)', ], [ - 'array', + 'array', 'range($integer, $mixed)', ], [ @@ -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 @@ -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 diff --git a/tests/PHPStan/Analyser/data/bug-2378.php b/tests/PHPStan/Analyser/data/bug-2378.php new file mode 100644 index 0000000000..4e5dac9407 --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-2378.php @@ -0,0 +1,23 @@ +', range($s, $s)); + } + +}