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

Refactor: constant array type inference #6279

Merged
merged 9 commits into from
Aug 11, 2021
354 changes: 208 additions & 146 deletions src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\NonDivArithmeticOpAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Type\TypeCombiner;
use Psalm\StatementsSource;
use Psalm\Storage\ClassConstantStorage;
use Psalm\Type;

use function array_merge;
use function array_shift;
use function array_values;
use function count;
use function reset;
use function strtolower;
Expand Down Expand Up @@ -297,152 +300,15 @@ public static function infer(
}

if ($stmt instanceof PhpParser\Node\Expr\Array_) {
if (count($stmt->items) === 0) {
return Type::getEmptyArray();
}

$item_key_type = null;
$item_value_type = null;

$property_types = [];
$class_strings = [];

$can_create_objectlike = true;

$is_list = true;

foreach ($stmt->items as $int_offset => $item) {
if ($item === null) {
continue;
}

$single_item_key_type = null;

if ($item->key) {
$single_item_key_type = self::infer(
$codebase,
$nodes,
$item->key,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
);

if ($single_item_key_type) {
if ($item_key_type) {
$item_key_type = Type::combineUnionTypes(
$single_item_key_type,
$item_key_type,
null,
false,
true,
30
);
} else {
$item_key_type = $single_item_key_type;
}
}
} else {
$item_key_type = Type::getInt();
}

$single_item_value_type = self::infer(
$codebase,
$nodes,
$item->value,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
);

if (!$single_item_value_type) {
return null;
}

if ($item->key instanceof PhpParser\Node\Scalar\String_
|| $item->key instanceof PhpParser\Node\Scalar\LNumber
|| !$item->key
) {
if (count($property_types) <= 50) {
$property_types[$item->key ? $item->key->value : $int_offset] = $single_item_value_type;
} else {
$can_create_objectlike = false;
}

if ($item->key
&& (!$item->key instanceof PhpParser\Node\Scalar\LNumber
|| $item->key->value !== $int_offset)
) {
$is_list = false;
}
} else {
$is_list = false;
$dim_type = $single_item_key_type;

if (!$dim_type) {
return null;
}

$dim_atomic_types = $dim_type->getAtomicTypes();

if (count($dim_atomic_types) > 1 || $dim_type->hasMixed() || count($property_types) > 50) {
$can_create_objectlike = false;
} else {
$atomic_type = array_shift($dim_atomic_types);

if ($atomic_type instanceof Type\Atomic\TLiteralInt
|| $atomic_type instanceof Type\Atomic\TLiteralString
) {
if ($atomic_type instanceof Type\Atomic\TLiteralClassString) {
$class_strings[$atomic_type->value] = true;
}

$property_types[$atomic_type->value] = $single_item_value_type;
} else {
$can_create_objectlike = false;
}
}
}

if ($item_value_type) {
$item_value_type = Type::combineUnionTypes(
$single_item_value_type,
$item_value_type,
null,
false,
true,
30
);
} else {
$item_value_type = $single_item_value_type;
}
}

// if this array looks like an object-like array, let's return that instead
if ($item_value_type
&& $item_key_type
&& ($item_key_type->hasString() || $item_key_type->hasInt())
&& $can_create_objectlike
&& $property_types
) {
$objectlike = new Type\Atomic\TKeyedArray($property_types, $class_strings);
$objectlike->sealed = true;
$objectlike->is_list = $is_list;
return new Type\Union([$objectlike]);
}

if (!$item_key_type || !$item_value_type) {
return null;
}

return new Type\Union([
new Type\Atomic\TNonEmptyArray([
$item_key_type,
$item_value_type,
]),
]);
return self::inferArrayType(
$codebase,
$nodes,
$stmt,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
);
}

if ($stmt instanceof PhpParser\Node\Expr\Cast\Int_) {
Expand Down Expand Up @@ -547,4 +413,200 @@ public static function infer(

return null;
}

/**
* @param ?array<string, ClassConstantStorage> $existing_class_constants
*/
private static function inferArrayType(
\Psalm\Codebase $codebase,
\Psalm\Internal\Provider\NodeDataProvider $nodes,
PhpParser\Node\Expr\Array_ $stmt,
\Psalm\Aliases $aliases,
\Psalm\FileSource $file_source = null,
?array $existing_class_constants = null,
?string $fq_classlike_name = null
): ?Type\Union {
if (count($stmt->items) === 0) {
return Type::getEmptyArray();
}

$array_creation_info = new ArrayCreationInfo();

foreach ($stmt->items as $int_offset => $item) {
if ($item === null) {
continue;
}

if (!self::handleArrayItem(
$codebase,
$nodes,
$array_creation_info,
$int_offset,
$item,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
)) {
return null;
}
}

$item_key_type = null;
if ($array_creation_info->item_key_atomic_types) {
$item_key_type = TypeCombiner::combine(
$array_creation_info->item_key_atomic_types,
null,
false,
true,
30
);
}

$item_value_type = null;
if ($array_creation_info->item_value_atomic_types) {
$item_value_type = TypeCombiner::combine(
$array_creation_info->item_value_atomic_types,
null,
false,
true,
30
);
}

// if this array looks like an object-like array, let's return that instead
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe a similar check could be done for inferring TList?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You mean, for cases where it crossed the threshold for can_create_objectlike?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I saw multiple cases where can_create_objectlike can be false, so any of those I guess

if ($item_value_type
&& $item_key_type
&& ($item_key_type->hasString() || $item_key_type->hasInt())
&& $array_creation_info->can_create_objectlike
&& $array_creation_info->property_types
) {
$objectlike = new Type\Atomic\TKeyedArray(
$array_creation_info->property_types,
$array_creation_info->class_strings
);
$objectlike->sealed = true;
$objectlike->is_list = $array_creation_info->all_list;
return new Type\Union([$objectlike]);
}

if (!$item_key_type || !$item_value_type) {
return null;
}

return new Type\Union([
new Type\Atomic\TNonEmptyArray([
$item_key_type,
$item_value_type,
]),
]);
}

/**
* @param ?array<string, ClassConstantStorage> $existing_class_constants
*/
private static function handleArrayItem(
\Psalm\Codebase $codebase,
\Psalm\Internal\Provider\NodeDataProvider $nodes,
ArrayCreationInfo $array_creation_info,
int $int_offset,
PhpParser\Node\Expr\ArrayItem $item,
\Psalm\Aliases $aliases,
\Psalm\FileSource $file_source = null,
?array $existing_class_constants = null,
?string $fq_classlike_name = null
): bool {
$single_item_key_type = null;

if ($item->key) {
$single_item_key_type = self::infer(
$codebase,
$nodes,
$item->key,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
);

if ($single_item_key_type) {
$array_creation_info->item_key_atomic_types = array_merge(
$array_creation_info->item_key_atomic_types,
array_values($single_item_key_type->getAtomicTypes())
);
}
} else {
$array_creation_info->item_key_atomic_types[] = new Type\Atomic\TInt();
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could probably directly put a TLiteralInt with $int_offset in here I think

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Perhaps. That wouldn't be a refactoring in its strict sense though 😄

Copy link
Collaborator

Choose a reason for hiding this comment

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

fair enough :)

}

$single_item_value_type = self::infer(
$codebase,
$nodes,
$item->value,
$aliases,
$file_source,
$existing_class_constants,
$fq_classlike_name
);

if (!$single_item_value_type) {
return false;
}

if ($item->key instanceof PhpParser\Node\Scalar\String_
|| $item->key instanceof PhpParser\Node\Scalar\LNumber
|| !$item->key
) {
if (count($array_creation_info->property_types) <= 50) {
$key_value = $item->key ? $item->key->value : $int_offset;
$array_creation_info->property_types[$key_value] = $single_item_value_type;
} else {
$array_creation_info->can_create_objectlike = false;
}

if ($item->key
&& (!$item->key instanceof PhpParser\Node\Scalar\LNumber
|| $item->key->value !== $int_offset)
) {
$array_creation_info->all_list = false;
}
} else {
$array_creation_info->all_list = false;
$dim_type = $single_item_key_type;

if (!$dim_type) {
return false;
}

$dim_atomic_types = $dim_type->getAtomicTypes();

if (count($dim_atomic_types) > 1
|| $dim_type->hasMixed()
|| count($array_creation_info->property_types) > 50
) {
$array_creation_info->can_create_objectlike = false;
} else {
$atomic_type = array_shift($dim_atomic_types);

if ($atomic_type instanceof Type\Atomic\TLiteralInt
|| $atomic_type instanceof Type\Atomic\TLiteralString
) {
if ($atomic_type instanceof Type\Atomic\TLiteralClassString) {
$array_creation_info->class_strings[$atomic_type->value] = true;
}

$array_creation_info->property_types[$atomic_type->value] = $single_item_value_type;
} else {
$array_creation_info->can_create_objectlike = false;
}
}
}

$array_creation_info->item_value_atomic_types = array_merge(
$array_creation_info->item_value_atomic_types,
array_values($single_item_value_type->getAtomicTypes())
);

return true;
}
}