Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

array_fill(): handle negative cases, support integer ranges and non-empty-array #603

Merged
merged 12 commits into from
Aug 20, 2021
2 changes: 1 addition & 1 deletion resources/functionMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
'array_diff_uassoc\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable'],
'array_diff_ukey' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'key_comp_func'=>'callable(mixed,mixed):int'],
'array_diff_ukey\'1' => ['array', 'arr1'=>'array', 'arr2'=>'array', 'arr3'=>'array', 'arg4'=>'array|callable(mixed,mixed):int', '...rest='=>'array|callable(mixed,mixed):int'],
'array_fill' => ['array', 'start_key'=>'int', 'num'=>'int', 'val'=>'mixed'],
'array_fill' => ['array', 'start_index'=>'int', 'count'=>'int', 'value'=>'mixed'],
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adjusted the param names to the ones named on php.net: https://www.php.net/manual/en/function.array-fill.php

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want this. The parameter names are correct because on PHP 8 these signatures are merged with https://github.com/phpstan/php-8-stubs. So there's no need for this change.

'array_fill_keys' => ['array', 'keys'=>'array', 'val'=>'mixed'],
'array_filter' => ['array', 'input'=>'array', 'callback='=>'callable(mixed,mixed):bool|callable(mixed):bool', 'flag='=>'int'],
'array_flip' => ['array', 'input'=>'array<int|string>'],
Expand Down
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_index'=>'int', 'count'=>'0|positive-int', 'value'=>'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_index'=>'int', 'count'=>'int', 'value'=>'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
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([
staabm marked this conversation as resolved.
Show resolved Hide resolved
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