Skip to content

Commit

Permalink
Literal inferring for array_column, array_combine, array_fill_keys, a…
Browse files Browse the repository at this point in the history
…rray_fill (#8850)

* Squash

* Remove BC break

* Suppress

* Possibly fix

* Fixes

* Fix test

* Implement literal array_column

* Improve array_column inference

* Improve logic

* Add array_combine return type provider

* Cleanup

* Fix #8868 (add array_fill_keys/array_fill return type)

* cs-fix

* Fix
  • Loading branch information
danog committed Dec 13, 2022
1 parent cca2767 commit 6347a21
Show file tree
Hide file tree
Showing 7 changed files with 596 additions and 78 deletions.
4 changes: 4 additions & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Expand Up @@ -8,6 +8,8 @@
use Psalm\Context;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayChunkReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayColumnReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayCombineReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayFillKeysReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayFillReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayFilterReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\ArrayMapReturnTypeProvider;
Expand Down Expand Up @@ -68,6 +70,7 @@ public function __construct()

$this->registerClass(ArrayChunkReturnTypeProvider::class);
$this->registerClass(ArrayColumnReturnTypeProvider::class);
$this->registerClass(ArrayCombineReturnTypeProvider::class);
$this->registerClass(ArrayFilterReturnTypeProvider::class);
$this->registerClass(ArrayMapReturnTypeProvider::class);
$this->registerClass(ArrayMergeReturnTypeProvider::class);
Expand All @@ -81,6 +84,7 @@ public function __construct()
$this->registerClass(ArrayReverseReturnTypeProvider::class);
$this->registerClass(ArrayUniqueReturnTypeProvider::class);
$this->registerClass(ArrayFillReturnTypeProvider::class);
$this->registerClass(ArrayFillKeysReturnTypeProvider::class);
$this->registerClass(FilterVarReturnTypeProvider::class);
$this->registerClass(IteratorToArrayReturnTypeProvider::class);
$this->registerClass(ParseUrlReturnTypeProvider::class);
Expand Down
Expand Up @@ -2,17 +2,21 @@

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Internal\Analyzer\SourceAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TClassStringMap;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Union;

use function count;
use function reset;

/**
* @internal
Expand All @@ -36,42 +40,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
) {
return Type::getMixed();
}

$row_type = $row_shape = null;
$input_array_not_empty = false;

// calculate row shape
if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value))
&& $first_arg_type->isSingle()
&& $first_arg_type->hasArray()
) {
$input_array = $first_arg_type->getArray();
if ($input_array instanceof TKeyedArray) {
$row_type = $input_array->getGenericValueType();
} elseif ($input_array instanceof TArray) {
$row_type = $input_array->type_params[1];
}

if ($row_type && $row_type->isSingle()) {
if ($row_type->hasArray()) {
$row_shape = $row_type->getArray();
} elseif ($row_type->hasObjectType()) {
$row_shape_union = GetObjectVarsReturnTypeProvider::getGetObjectVarsReturnType(
$row_type,
$statements_source,
$event->getContext(),
$event->getCodeLocation()
);
if ($row_shape_union->isSingle()) {
$row_shape_union_parts = $row_shape_union->getAtomicTypes();
$row_shape = reset($row_shape_union_parts);
}
}
}

$input_array_not_empty = $input_array instanceof TNonEmptyArray ||
($input_array instanceof TKeyedArray && $input_array->isNonEmpty());
}
$context = $event->getContext();
$code_location = $event->getCodeLocation();

$value_column_name = null;
$value_column_name_is_null = false;
Expand All @@ -86,6 +56,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
}

$key_column_name = null;
$key_column_name_is_null = false;
$third_arg_type = null;
// calculate key column name
if (isset($call_args[2])) {
Expand All @@ -97,9 +68,125 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
} elseif ($third_arg_type->isSingleStringLiteral()) {
$key_column_name = $third_arg_type->getSingleStringLiteral()->value;
}
$key_column_name_is_null = $third_arg_type->isNull();
}
}


$row_type = $row_shape = null;
$input_array_not_empty = false;

// calculate row shape
if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value))
&& $first_arg_type->isSingle()
&& $first_arg_type->hasArray()
) {
$input_array = $first_arg_type->getArray();
if ($input_array instanceof TKeyedArray && !$input_array->fallback_params
&& ($value_column_name !== null || $value_column_name_is_null)
&& !($third_arg_type && !$key_column_name)
) {
$properties = [];
$ok = true;
$last_custom_key = -1;
$is_list = $input_array->is_list || $key_column_name !== null;
$had_possibly_undefined = false;
foreach ($input_array->properties as $key => $property) {
$row_shape = self::getRowShape(
$property,
$statements_source,
$context,
$code_location
);
if (!$row_shape) {
continue;
}
if (!$row_shape instanceof TKeyedArray) {
if ($row_shape instanceof TArray && $row_shape->isEmptyArray()) {
continue;
}
$ok = false;
break;
}

if ($value_column_name !== null) {
if (isset($row_shape->properties[$value_column_name])) {
$result_element_type = $row_shape->properties[$value_column_name];
} elseif ($row_shape->fallback_params) {
$ok = false;
break;
} else {
continue;
}
} else {
$result_element_type = $property;
}

if ($key_column_name !== null) {
if (isset($row_shape->properties[$key_column_name])) {
$result_key_type = $row_shape->properties[$key_column_name];
if ($result_key_type->isSingleIntLiteral()) {
$key = $result_key_type->getSingleIntLiteral()->value;
if ($is_list && $last_custom_key != $key-1) {
$is_list = false;
}
$last_custom_key = $key;
} elseif ($result_key_type->isSingleStringLiteral()) {
$key = $result_key_type->getSingleStringLiteral()->value;
$is_list = false;
} else {
$ok = false;
break;
}
} else {
$ok = false;
break;
}
}

$properties[$key] = $result_element_type->setPossiblyUndefined(
$property->possibly_undefined
);

if (!$property->possibly_undefined
&& $had_possibly_undefined
) {
$is_list = false;
}

$had_possibly_undefined = $had_possibly_undefined || $property->possibly_undefined;
}
if ($ok) {
if (!$properties) {
return Type::getEmptyArray();
}
return new Union([new TKeyedArray(
$properties,
null,
$input_array->fallback_params,
$is_list
)]);
}
}

if ($input_array instanceof TKeyedArray) {
$row_type = $input_array->getGenericValueType();
} elseif ($input_array instanceof TArray) {
$row_type = $input_array->type_params[1];
}

$row_shape = self::getRowShape(
$row_type,
$statements_source,
$context,
$code_location
);

$input_array_not_empty = $input_array instanceof TNonEmptyArray ||
($input_array instanceof TKeyedArray && $input_array->isNonEmpty());
}


$result_key_type = Type::getArrayKey();
$result_element_type = null !== $row_type && $value_column_name_is_null ? $row_type : null;
$have_at_least_one_res = false;
Expand All @@ -122,7 +209,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
}
}

if (isset($call_args[2]) && (string)$third_arg_type !== 'null') {
if ($third_arg_type && !$key_column_name_is_null) {
$type = $have_at_least_one_res ?
new TNonEmptyArray([$result_key_type, $result_element_type ?? Type::getMixed()])
: new TArray([$result_key_type, $result_element_type ?? Type::getMixed()]);
Expand All @@ -134,4 +221,28 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev

return new Union([$type]);
}

/**
* @return TArray|TKeyedArray|TClassStringMap|null
*/
private static function getRowShape(
?Union $row_type,
SourceAnalyzer $statements_source,
Context $context,
CodeLocation $code_location
): ?Atomic {
if ($row_type && $row_type->isSingle()) {
if ($row_type->hasArray()) {
return $row_type->getArray();
} elseif ($row_type->hasObjectType()) {
return GetObjectVarsReturnTypeProvider::getGetObjectVarsReturnType(
$row_type,
$statements_source,
$context,
$code_location
);
}
}
return null;
}
}
@@ -0,0 +1,132 @@
<?php

namespace Psalm\Internal\Provider\ReturnTypeProvider;

use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Issue\InvalidArgument;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Union;

use function array_combine;
use function assert;
use function count;

/**
* @internal
*/
class ArrayCombineReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return ['array_combine'];
}

public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
{
$statements_source = $event->getStatementsSource();
$call_args = $event->getCallArgs();
if (!$statements_source instanceof StatementsAnalyzer
|| count($call_args) < 2
) {
return Type::getNever();
}

if (!$keys_type = $statements_source->node_data->getType($call_args[0]->value)) {
return null;
}
if (!$keys_type->isArray()) {
return null;
}

$keys = $keys_type->getArray();
if ($keys instanceof TArray && $keys->isEmptyArray()) {
$keys = [];
} elseif (!$keys instanceof TKeyedArray || $keys->fallback_params) {
return null;
} else {
$keys = $keys->properties;
}

if (!$values_type = $statements_source->node_data->getType($call_args[1]->value)) {
return null;
}
if (!$values_type->isArray()) {
return null;
}

$values = $values_type->getArray();
if ($values instanceof TArray && $values->isEmptyArray()) {
$values = [];
} elseif (!$values instanceof TKeyedArray || $values->fallback_params) {
return null;
} else {
$values = $values->properties;
}


$keys_array = [];
$is_list = true;
$prev_key = -1;
foreach ($keys as $key) {
if ($key->possibly_undefined) {
return null;
}
if ($key->isSingleIntLiteral()) {
$key = $key->getSingleIntLiteral()->value;
$keys_array []= $key;
if ($is_list && $key-1 !== $prev_key) {
$is_list = false;
}
$prev_key = $key;
} elseif ($key->isSingleStringLiteral()) {
$keys_array []= $key->getSingleStringLiteral()->value;
$is_list = false;
} else {
return null;
}
}

foreach ($values as $value) {
if ($value->possibly_undefined) {
return null;
}
}

if (count($keys_array) !== count($values)) {
IssueBuffer::maybeAdd(
new InvalidArgument(
'The keys array ' . $keys_type->getId() . ' must have exactly the same '
. 'number of elements as the values array '
. $values_type->getId(),
$event->getCodeLocation(),
'array_combine'
),
$statements_source->getSuppressedIssues()
);
return $statements_source->getCodebase()->analysis_php_version_id >= 8_00_00
? Type::getNever()
: Type::getFalse();
}

$result = array_combine(
$keys_array,
$values
);

assert($result !== false);

if (!$result) {
return Type::getEmptyArray();
}

return new Union([new TKeyedArray($result, null, null, $is_list)]);
}
}

0 comments on commit 6347a21

Please sign in to comment.