Skip to content
2 changes: 2 additions & 0 deletions resources/functionMap_php80delta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -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'],
Expand Down
3 changes: 3 additions & 0 deletions src/File/ParentDirectoryRelativePathHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
34 changes: 30 additions & 4 deletions src/Type/Php/ArrayFillFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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(),
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5100,10 +5100,34 @@ public function dataArrayFunctions(): array
'array(1, 1, 1, 1, 1)',
'$filledIntegers',
],
[
'array()',
'$emptyFilled',
],
[
'array(1)',
'$filledIntegersWithKeys',
],
[
'array<int, \'foo\'>&nonEmpty',
'$filledNonEmptyArray',
],
[
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
'$filledAlwaysFalse',
],
[
PHP_VERSION_ID < 80000 ? 'false' : '*NEVER*',
'$filledNegativeConstAlwaysFalse',
],
[
'array<int, 1>|false',
'$filledByMaybeNegativeRange',
],
[
'array<int, 1>&nonEmpty',
'$filledByPositiveRange',
],
[
'array(1, 2)',
'array_keys($integerKeys)',
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Analyser/data/array-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Parallel/SchedulerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> $expectedJobSizes
*/
Expand Down