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

Improve array-shape list reconcilation (fixes #8046). #8050

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
53 changes: 45 additions & 8 deletions src/Psalm/Internal/Type/SimpleAssertionReconciler.php
Expand Up @@ -78,6 +78,8 @@
use function count;
use function explode;
use function get_class;
use function is_int;
use function ksort;
use function min;
use function strpos;

Expand Down Expand Up @@ -358,6 +360,7 @@ public static function reconcile(
) {
return self::reconcileList(
$assertion,
$codebase,
$existing_var_type,
$key,
$negated,
Expand Down Expand Up @@ -2000,6 +2003,7 @@ private static function reconcileArray(
*/
private static function reconcileList(
Assertion $assertion,
Codebase $codebase,
Union $existing_var_type,
?string $key,
bool $negated,
Expand All @@ -2021,20 +2025,53 @@ private static function reconcileList(
$did_remove_type = false;

foreach ($existing_var_atomic_types as $type) {
if ($type instanceof TList
|| ($type instanceof TKeyedArray && $type->is_list)
) {
if ($is_non_empty && $type instanceof TList && !$type instanceof TNonEmptyList) {
if ($type instanceof TList) {
if ($is_non_empty && !$type instanceof TNonEmptyList) {
$array_types[] = new TNonEmptyList($type->type_param);
$did_remove_type = true;
} else {
$array_types[] = $type;
}
} elseif ($type instanceof TArray || $type instanceof TKeyedArray) {
if ($type instanceof TKeyedArray) {
$type = $type->getGenericArrayType();
}
} elseif ($type instanceof TKeyedArray) {
if ($type->is_list) {
$array_types[] = $type;
} else {
$did_remove_type = true;
$type = clone $type;
$min_unset_list_key = 0; // Minimum list key not explicitly set
ksort($type->properties);
foreach ($type->properties as $prop_key => $prop_value) {
orklah marked this conversation as resolved.
Show resolved Hide resolved
if (!is_int($prop_key)) {
if ($prop_value->possibly_undefined) {
unset($type->properties[$prop_key]);
} else {
// Can't reconcile, type is removed
continue 2;
}
} elseif ($prop_key === $min_unset_list_key) {
++$min_unset_list_key;
AndrolGenhald marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Update the key type for non-explicit properties
if ($type->previous_key_type === null) {
$type->previous_key_type = Type::getArrayKey();
}
$type->previous_key_type = Type::intersectUnionTypes(
$type->previous_key_type,
new Union([new TIntRange($min_unset_list_key, null)]),
$codebase,
);

// If there's no value type for non-explicit properties it's defaulted to mixed
if ($type->previous_value_type === null) {
$type->previous_value_type = Type::getMixed();
}

$type->is_list = true;
$array_types[] = $type;
}
} elseif ($type instanceof TArray) {
if ($type->type_params[0]->hasArrayKey()
|| $type->type_params[0]->hasInt()
) {
Expand Down
21 changes: 21 additions & 0 deletions tests/AssertAnnotationTest.php
Expand Up @@ -2055,6 +2055,27 @@ function makeLowerNonEmpty(string $input): string
return $input;
}',
],
'assertListKeepsArrayShape' => [
'code' => '<?php
/**
* @param mixed $arr
* @psalm-assert-if-true list $arr
*/
function is_list($arr): bool
{
return is_array($arr) && array_is_list($arr);
}

/** @var array{0: int, 1: bool, 2: string} */
$list = [1, true, "string"];
assert(is_list($list));

function takesString(string $_str): void {}

takesString($list[2]);
',
'assertions' => ['$list===' => "array{0: int, 1: bool, 2: string}<int<3, max>, mixed>"],
AndrolGenhald marked this conversation as resolved.
Show resolved Hide resolved
],
];
}

Expand Down