diff --git a/resources/functionMap_php80delta.php b/resources/functionMap_php80delta.php index f1735f2a4e..a90cd9af3d 100644 --- a/resources/functionMap_php80delta.php +++ b/resources/functionMap_php80delta.php @@ -22,6 +22,7 @@ return [ 'new' => [ 'array_combine' => ['associative-array', 'keys'=>'string[]|int[]', 'values'=>'array'], + 'array_fill' => ['array', 'start_key'=>'int', 'num'=>'0|positive-int', 'val'=>'mixed'], 'bcdiv' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], @@ -146,6 +147,7 @@ 'old' => [ 'array_combine' => ['associative-array|false', 'keys'=>'string[]|int[]', 'values'=>'array'], + 'array_fill' => ['array', 'start_key'=>'int', 'num'=>'int', 'val'=>'mixed'], 'bcdiv' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcmod' => ['?string', 'dividend'=>'string', 'divisor'=>'string', 'scale='=>'int'], 'bcpowmod' => ['?string', 'base'=>'string', 'exponent'=>'string', 'modulus'=>'string', 'scale='=>'int'], diff --git a/src/File/ParentDirectoryRelativePathHelper.php b/src/File/ParentDirectoryRelativePathHelper.php index 306f8f0dbf..b88c930758 100644 --- a/src/File/ParentDirectoryRelativePathHelper.php +++ b/src/File/ParentDirectoryRelativePathHelper.php @@ -54,6 +54,9 @@ public function getFilenameParts(string $filename): array } $dotsCount = $parentPartsCount - $i; + if ($dotsCount < 0) { + throw new \PHPStan\ShouldNotHappenException(); + } return array_merge(array_fill(0, $dotsCount, '..'), array_slice($filenameParts, $i)); } diff --git a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php index c1a8c168dd..ab42e9a5d9 100644 --- a/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayFillFunctionReturnTypeExtension.php @@ -4,21 +4,33 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\Type\Accessory\NonEmptyArrayType; use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayTypeBuilder; +use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\NeverType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; class ArrayFillFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension { private const MAX_SIZE_USE_CONSTANT_ARRAY = 100; + private PhpVersion $phpVersion; + + public function __construct(PhpVersion $phpVersion) + { + $this->phpVersion = $phpVersion; + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_fill'; @@ -34,6 +46,23 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, $numberType = $scope->getType($functionCall->args[1]->value); $valueType = $scope->getType($functionCall->args[2]->value); + if ($numberType instanceof IntegerRangeType) { + if ($numberType->getMin() < 0) { + return TypeCombinator::union( + new ArrayType(new IntegerType(), $valueType), + new ConstantBooleanType(false) + ); + } + } + + // check against negative-int, which is not allowed + if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($numberType)->yes()) { + if ($this->phpVersion->throwsValueErrorForInternalFunctions()) { + return new NeverType(); + } + return new ConstantBooleanType(false); + } + if ( $startIndexType instanceof ConstantIntegerType && $numberType instanceof ConstantIntegerType @@ -56,10 +85,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection, return $arrayBuilder->getArray(); } - if ( - $numberType instanceof ConstantIntegerType - && $numberType->getValue() > 0 - ) { + if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($numberType)->yes()) { return new IntersectionType([ new ArrayType(new IntegerType(), $valueType), new NonEmptyArrayType(), diff --git a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php index 27983a2c6a..6971ea40f0 100644 --- a/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php @@ -5100,10 +5100,34 @@ public function dataArrayFunctions(): array 'array(1, 1, 1, 1, 1)', '$filledIntegers', ], + [ + 'array()', + '$emptyFilled', + ], [ 'array(1)', '$filledIntegersWithKeys', ], + [ + 'array&nonEmpty', + '$filledNonEmptyArray', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledAlwaysFalse', + ], + [ + PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*', + '$filledNegativeConstAlwaysFalse', + ], + [ + 'array|false', + '$filledByMaybeNegativeRange', + ], + [ + 'array&nonEmpty', + '$filledByPositiveRange', + ], [ 'array(1, 2)', 'array_keys($integerKeys)', diff --git a/tests/PHPStan/Analyser/data/array-functions.php b/tests/PHPStan/Analyser/data/array-functions.php index 752fe4fd07..15d7252487 100644 --- a/tests/PHPStan/Analyser/data/array-functions.php +++ b/tests/PHPStan/Analyser/data/array-functions.php @@ -37,7 +37,16 @@ }, 1); $filledIntegers = array_fill(0, 5, 1); +$emptyFilled = array_fill(3, 0, 'banana'); $filledIntegersWithKeys = array_fill_keys([0], 1); +/** @var negative-int $negInt */ +$filledAlwaysFalse = array_fill(0, $negInt, 1); +/** @var positive-int $posInt */ +$filledNonEmptyArray = array_fill(0, $posInt, 'foo'); +$filledNegativeConstAlwaysFalse = array_fill(0, -5, 1); +/** @var int<-3, 5> $maybeNegRange */ +$filledByMaybeNegativeRange = array_fill(0, $maybeNegRange, 1); +$filledByPositiveRange = array_fill(0, rand(3, 5), 1); $integerKeys = [ 1 => 'foo', diff --git a/tests/PHPStan/Parallel/SchedulerTest.php b/tests/PHPStan/Parallel/SchedulerTest.php index e029e06d9b..538dd69016 100644 --- a/tests/PHPStan/Parallel/SchedulerTest.php +++ b/tests/PHPStan/Parallel/SchedulerTest.php @@ -73,7 +73,7 @@ public function dataSchedule(): array * @param int $maximumNumberOfProcesses * @param int $minimumNumberOfJobsPerProcess * @param int $jobSize - * @param int $numberOfFiles + * @param 0|positive-int $numberOfFiles * @param int $expectedNumberOfProcesses * @param array $expectedJobSizes */