diff --git a/src/Type/Php/RangeFunctionReturnTypeExtension.php b/src/Type/Php/RangeFunctionReturnTypeExtension.php index 65a7ab3236..50dd6579ec 100644 --- a/src/Type/Php/RangeFunctionReturnTypeExtension.php +++ b/src/Type/Php/RangeFunctionReturnTypeExtension.php @@ -12,6 +12,7 @@ use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\FloatType; use PHPStan\Type\IntegerType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\TypeUtils; @@ -86,7 +87,6 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, if ( $startType instanceof IntegerType - && $endType instanceof IntegerType && $stepType instanceof IntegerType ) { return new ArrayType(new IntegerType(), new IntegerType()); @@ -100,6 +100,12 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return new ArrayType(new IntegerType(), new FloatType()); } + if ($startType instanceof StringType + && $endType instanceof StringType + && $stepType instanceof IntegerType) { + return new ArrayType(new IntegerType(), new StringType()); + } + return new ArrayType(new IntegerType(), new UnionType([new IntegerType(), new FloatType()])); } diff --git a/tests/PHPStan/Type/Php/RangeFunctionReturnTypeExtensionTest.php b/tests/PHPStan/Type/Php/RangeFunctionReturnTypeExtensionTest.php new file mode 100644 index 0000000000..2440353592 --- /dev/null +++ b/tests/PHPStan/Type/Php/RangeFunctionReturnTypeExtensionTest.php @@ -0,0 +1,138 @@ +createMock(FunctionReflection::class); + $reflectionFunctionMock->expects($this->once())->method('getName')->willReturn('range'); + $this->extension->isFunctionSupported($reflectionFunctionMock); + } + + /** + * @phpstan-param class-string $startType + * @phpstan-param class-string $endType + * @phpstan-param class-string $stepType + * @phpstan-param class-string $returnType + * + * @dataProvider provideStartEndStopAndReturnType + */ + public function testArrayTypesDependingOnStartEndAndStepType( + string $startType, + string $endType, + string $stepType, + string $returnType + ): void { + $scope = $this->createMock(Scope::class); + $scope->method('getType') + ->willReturnOnConsecutiveCalls( + $this->createMock($startType), + $this->createMock($endType), + $this->createMock($stepType) + ); + + $type = $this->extension->getTypeFromFunctionCall($this->createMock(FunctionReflection::class), + $this->createFuncCallMock(), $scope); + + $this->assertInstanceOf(ArrayType::class, $type); + $this->assertInstanceOf(IntegerType::class, $type->getIterableKeyType()); + $this->assertInstanceOf($returnType, $type->getIterableValueType()); + } + + private function createFuncCallMock(): FuncCall + { + return new class('anything', [ + $this->createExprFromType(), + $this->createExprFromType(), + $this->createExprFromType() + ]) extends FuncCall { + }; + } + + /** + * @param string $argType + * + * @phpstan-param class-string $className + * @return Arg + * + */ + private function createExprFromType(): Arg + { + $expr = $this->createMock(Arg::class); + $expr->value = $this->createMock(Expr::class); + + return $expr; + } + + public function provideStartEndStopAndReturnType(): array + { + return [ + 'int,int,int' => [ + 'startType' => IntegerType::class, + 'endType' => IntegerType::class, + 'stepType' => IntegerType::class, + 'returnType' => IntegerType::class, + ], + 'float,int,int' => [ + 'startType' => FloatType::class, + 'endType' => IntegerType::class, + 'stepType' => IntegerType::class, + 'returnType' => FloatType::class, + ], + 'int,float,int' => [ + 'startType' => IntegerType::class, + 'endType' => FloatType::class, + 'stepType' => IntegerType::class, + 'returnType' => IntegerType::class, + ], + 'float,float,float' => [ + 'startType' => FloatType::class, + 'endType' => FloatType::class, + 'stepType' => FloatType::class, + 'returnType' => FloatType::class, + ], + 'float,int,float' => [ + 'startType' => FloatType::class, + 'endType' => IntegerType::class, + 'stepType' => FloatType::class, + 'returnType' => FloatType::class, + ], + 'int,float,float' => [ + 'startType' => IntegerType::class, + 'endType' => FloatType::class, + 'stepType' => FloatType::class, + 'returnType' => FloatType::class, + ], + 'string,string,int' => [ + 'startType' => StringType::class, + 'endType' => StringType::class, + 'stepType' => IntegerType::class, + 'returnType' => StringType::class, + ] + ]; + } + + protected function setUp(): void + { + $this->extension = new RangeFunctionReturnTypeExtension(); + } +}