Skip to content
Permalink
Browse files

Improve array function list handling (#2377)

* array_column() returns a list unless the 3rd arg is passed

* array_pad() return type provider

* array_chunk() return type provider

* array_map() preserve list types
  • Loading branch information
ShiraNai7 authored and muglug committed Nov 26, 2019
1 parent 2f02da6 commit 4e594e0a65451909e89e3109915d20b8374bb18b
@@ -31,10 +31,12 @@ public function __construct()
{
self::$handlers = [];
$this->registerClass(ReturnTypeProvider\ArrayChunkReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayColumnReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayFilterReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayMapReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayMergeReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayPadReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayPointerAdjustmentReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayPopReturnTypeProvider::class);
$this->registerClass(ReturnTypeProvider\ArrayRandReturnTypeProvider::class);
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use function count;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Type\ArrayType;
use Psalm\StatementsSource;
use Psalm\Type;
class ArrayChunkReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTypeProviderInterface
{
public static function getFunctionIds(): array
{
return ['array_chunk'];
}
public static function getFunctionReturnType(
StatementsSource $statements_source,
string $function_id,
array $call_args,
Context $context,
CodeLocation $code_location
) {
if (count($call_args) >= 2
&& ($array_arg_type = $statements_source->getNodeTypeProvider()->getType($call_args[0]->value))
&& $array_arg_type->isSingle()
&& $array_arg_type->hasArray()
&& ($array_type = ArrayType::infer($array_arg_type->getTypes()['array']))
) {
$preserve_keys = isset($call_args[2])
&& ($preserve_keys_arg_type = $statements_source->getNodeTypeProvider()->getType($call_args[2]->value))
&& (string) $preserve_keys_arg_type !== 'false';
return new Type\Union([
new Type\Atomic\TList(
new Type\Union([
$preserve_keys
? new Type\Atomic\TNonEmptyArray([$array_type->key, $array_type->value])
: new Type\Atomic\TNonEmptyList($array_type->value)
])
)
]);
}
return new Type\Union([new Type\Atomic\TList(Type::getArray())]);
}
}
@@ -67,6 +67,7 @@ public static function getFunctionReturnType(
}
$key_column_name = null;
$third_arg_type = null;
// calculate key column name
if (isset($call_args[2])
&& ($third_arg_type = $statements_source->node_data->getType($call_args[2]->value))
@@ -93,19 +94,10 @@ public static function getFunctionReturnType(
}
}
if ($result_element_type) {
return new Type\Union([
new Type\Atomic\TArray([
$result_key_type,
$result_element_type,
]),
]);
}
$callmap_callables = CallMap::getCallablesFromCallMap($function_id);
assert($callmap_callables && $callmap_callables[0]->return_type);
return $callmap_callables[0]->return_type;
return new Type\Union([
isset($call_args[2]) && (string) $third_arg_type !== 'null'
? new Type\Atomic\TArray([$result_key_type, $result_element_type ?? Type::getMixed()])
: new Type\Atomic\TList($result_element_type ?? Type::getMixed())
]);
}
}
@@ -11,6 +11,7 @@
use Psalm\Internal\Analyzer\Statements\Expression\CallAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Codebase\CallMap;
use Psalm\Internal\Type\ArrayType;
use Psalm\StatementsSource;
use Psalm\Type;
use function strpos;
@@ -41,32 +42,22 @@ public static function getFunctionReturnType(
$array_arg = isset($call_args[1]->value) ? $call_args[1]->value : null;
$array_arg_atomic_type = null;
$array_arg_type = null;
if ($array_arg && ($array_arg_type = $statements_source->node_data->getType($array_arg))) {
$arg_types = $array_arg_type->getTypes();
if ($array_arg && ($array_arg_union_type = $statements_source->node_data->getType($array_arg))) {
$arg_types = $array_arg_union_type->getTypes();
if (isset($arg_types['array'])
&& ($arg_types['array'] instanceof Type\Atomic\TArray
|| $arg_types['array'] instanceof Type\Atomic\ObjectLike
|| $arg_types['array'] instanceof Type\Atomic\TList)
) {
if (isset($arg_types['array'])) {
$array_arg_atomic_type = $arg_types['array'];
$array_arg_type = ArrayType::infer($array_arg_atomic_type);
}
}
if (isset($call_args[0])) {
$function_call_arg = $call_args[0];
if (count($call_args) === 2) {
if ($array_arg_atomic_type instanceof Type\Atomic\ObjectLike) {
$generic_key_type = $array_arg_atomic_type->getGenericKeyType();
} elseif ($array_arg_atomic_type instanceof Type\Atomic\TList) {
$generic_key_type = Type::getInt();
} else {
$generic_key_type = $array_arg_atomic_type
? clone $array_arg_atomic_type->type_params[0]
: Type::getArrayKey();
}
$generic_key_type = $array_arg_type->key ?? Type::getArrayKey();
} else {
$generic_key_type = Type::getInt();
}
@@ -84,19 +75,20 @@ public static function getFunctionReturnType(
$inner_type = clone $closure_return_type;
if ($array_arg_atomic_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($inner_type) {
return clone $inner_type;
},
$array_arg_atomic_type->properties
)
),
]);
$atomic_type = new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($inner_type) {
return clone $inner_type;
},
$array_arg_atomic_type->properties
)
);
$atomic_type->is_list = $array_arg_atomic_type->is_list;
return new Type\Union([$atomic_type]);
}
if ($array_arg_atomic_type instanceof Type\Atomic\TList) {
@@ -258,31 +250,41 @@ function (Type\Union $_) use ($inner_type) {
if ($mapping_return_type) {
if ($array_arg_atomic_type instanceof Type\Atomic\ObjectLike && count($call_args) === 2) {
return new Type\Union([
new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($mapping_return_type) {
return clone $mapping_return_type;
},
$array_arg_atomic_type->properties
)
),
]);
$atomic_type = new Type\Atomic\ObjectLike(
array_map(
/**
* @return Type\Union
*/
function (Type\Union $_) use ($mapping_return_type) {
return clone $mapping_return_type;
},
$array_arg_atomic_type->properties
)
);
$atomic_type->is_list = $array_arg_atomic_type->is_list;
return new Type\Union([$atomic_type]);
}
return new Type\Union([
new Type\Atomic\TArray([
$generic_key_type,
$mapping_return_type,
]),
count($call_args) === 2 && !($array_arg_type->is_list ?? false)
? new Type\Atomic\TArray([
$generic_key_type,
$mapping_return_type,
])
: new Type\Atomic\TList($mapping_return_type)
]);
}
}
}
return Type::getArray();
return count($call_args) === 2 && !($array_arg_type->is_list ?? false)
? new Type\Union([
new Type\Atomic\TArray([
$array_arg_type->key ?? Type::getArrayKey(),
Type::getMixed(),
])
])
: Type::getList();
}
}
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use function count;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Type\ArrayType;
use Psalm\StatementsSource;
use Psalm\Type;
class ArrayPadReturnTypeProvider implements \Psalm\Plugin\Hook\FunctionReturnTypeProviderInterface
{
public static function getFunctionIds(): array
{
return ['array_pad'];
}
public static function getFunctionReturnType(
StatementsSource $statements_source,
string $function_id,
array $call_args,
Context $context,
CodeLocation $code_location
) {
$type_provider = $statements_source->getNodeTypeProvider();
if (count($call_args) >= 3
&& ($array_arg_type = $type_provider->getType($call_args[0]->value))
&& ($size_arg_type = $type_provider->getType($call_args[1]->value))
&& ($value_arg_type = $type_provider->getType($call_args[2]->value))
&& $array_arg_type->isSingle()
&& $array_arg_type->hasArray()
&& ($array_type = ArrayType::infer($array_arg_type->getTypes()['array']))
) {
$codebase = $statements_source->getCodebase();
$key_type = Type::combineUnionTypes($array_type->key, Type::getInt(), $codebase);
$value_type = Type::combineUnionTypes($array_type->value, $value_arg_type, $codebase);
$can_return_empty = (
!$size_arg_type->isSingleIntLiteral()
|| $size_arg_type->getSingleIntLiteral()->value === 0
);
return new Type\Union([
$array_type->is_list
? (
$can_return_empty
? new Type\Atomic\TList($value_type)
: new Type\Atomic\TNonEmptyList($value_type)
)
: (
$can_return_empty
? new Type\Atomic\TArray([$key_type, $value_type])
: new Type\Atomic\TNonEmptyArray([$key_type, $value_type])
)
]);
}
return Type::getArray();
}
}
@@ -152,18 +152,6 @@ function array_change_key_case(array $arr, int $case = CASE_LOWER)
{
}
/**
* @psalm-template T
*
* @param array<array-key, T> $arr
*
* @return array<int, array<array-key, T>>
* @psalm-pure
*/
function array_chunk(array $arr, int $size, bool $preserve_keys = false)
{
}
/**
* @psalm-template TKey as array-key
*
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace Psalm\Internal\Type;
use Psalm\Type;
/**
* @internal
*/
class ArrayType
{
/** @var Type\Union */
public $key;
/** @var Type\Union */
public $value;
/** @var bool */
public $is_list;
public function __construct(Type\Union $key, Type\Union $value, bool $is_list)
{
$this->key = $key;
$this->value = $value;
$this->is_list = $is_list;
}
/**
* @return static|null
*/
public static function infer(Type\Atomic $type): ?self
{
if ($type instanceof Type\Atomic\ObjectLike) {
return new static(
$type->getGenericKeyType(),
$type->getGenericValueType(),
$type->is_list
);
}
if ($type instanceof Type\Atomic\TList) {
return new static(
Type::getInt(),
$type->type_param,
true
);
}
if ($type instanceof Type\Atomic\TArray) {
return new static(
$type->type_params[0],
$type->type_params[1],
false
);
}
return null;
}
}

0 comments on commit 4e594e0

Please sign in to comment.
You can’t perform that action at this time.