Skip to content

Commit

Permalink
Merge pull request #7699 from AndrolGenhald/bugfix/int-range-unpacking
Browse files Browse the repository at this point in the history
  • Loading branch information
weirdan committed Feb 18, 2022
2 parents 6a68287 + 9310a4f commit 97bd81c
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 46 deletions.
47 changes: 22 additions & 25 deletions src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php
Expand Up @@ -93,6 +93,28 @@ public static function analyze(
);
}

// if this array looks like an object-like array, let's return that instead
if ($array_creation_info->can_create_objectlike
&& $array_creation_info->property_types
) {
$object_like = new TKeyedArray(
$array_creation_info->property_types,
$array_creation_info->class_strings
);
$object_like->sealed = true;
$object_like->is_list = $array_creation_info->all_list;

$stmt_type = new Union([$object_like]);

if ($array_creation_info->parent_taint_nodes) {
$stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes;
}

$statements_analyzer->node_data->setType($stmt, $stmt_type);

return true;
}

if ($array_creation_info->item_key_atomic_types) {
$item_key_type = TypeCombiner::combine(
$array_creation_info->item_key_atomic_types,
Expand Down Expand Up @@ -123,31 +145,6 @@ public static function analyze(
return true;
}

// 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())
&& $array_creation_info->can_create_objectlike
&& $array_creation_info->property_types
) {
$object_like = new TKeyedArray(
$array_creation_info->property_types,
$array_creation_info->class_strings
);
$object_like->sealed = true;
$object_like->is_list = $array_creation_info->all_list;

$stmt_type = new Union([$object_like]);

if ($array_creation_info->parent_taint_nodes) {
$stmt_type->parent_nodes = $array_creation_info->parent_taint_nodes;
}

$statements_analyzer->node_data->setType($stmt, $stmt_type);

return true;
}

if ($array_creation_info->all_list) {
if (empty($array_creation_info->item_key_atomic_types)) {
$array_type = new TList($item_value_type ?? Type::getMixed());
Expand Down
Expand Up @@ -8,8 +8,11 @@
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Union;
use UnexpectedValueException;
Expand Down Expand Up @@ -49,7 +52,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
&& ($array_arg_type = $nodeTypeProvider->getType($call_args[0]->value))
&& $array_arg_type->isSingle()
&& $array_arg_type->hasArray()
&& ($array_type = ArrayType::infer($array_arg_type->getAtomicTypes()['array']))
&& ($array_type = ArrayType::infer($array_arg_type->getSingleAtomic()))
) {
return $array_type->value;
}
Expand All @@ -58,24 +61,43 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
$min_bounds = [];
$max_bounds = [];
foreach ($call_args as $arg) {
if ($array_arg_type = $nodeTypeProvider->getType($arg->value)) {
foreach ($array_arg_type->getAtomicTypes() as $atomic_type) {
if (!$atomic_type instanceof TInt) {
$all_int = false;
break;
}

if ($atomic_type instanceof TLiteralInt) {
$min_bounds[] = $atomic_type->value;
$max_bounds[] = $atomic_type->value;
} elseif ($atomic_type instanceof TIntRange) {
$min_bounds[] = $atomic_type->min_bound;
$max_bounds[] = $atomic_type->max_bound;
} elseif (get_class($atomic_type) === TInt::class) {
$min_bounds[] = null;
$max_bounds[] = null;
if ($arg_type = $nodeTypeProvider->getType($arg->value)) {
if ($arg->unpack) {
if (!$arg_type->isSingle() || !$arg_type->isArray()) {
return Type::getMixed();
} else {
throw new UnexpectedValueException('Unexpected type');
$array_arg_type = $arg_type->getSingleAtomic();
if ($array_arg_type instanceof TKeyedArray) {
$possibly_unpacked_arg_types = $array_arg_type->properties;
} elseif ($array_arg_type instanceof TList) {
$possibly_unpacked_arg_types = [$array_arg_type->type_param];
} else {
assert($array_arg_type instanceof TArray);
$possibly_unpacked_arg_types = [$array_arg_type->type_params[1]];
}
}
} else {
$possibly_unpacked_arg_types = [$arg_type];
}
foreach ($possibly_unpacked_arg_types as $possibly_unpacked_arg_type) {
foreach ($possibly_unpacked_arg_type->getAtomicTypes() as $atomic_type) {
if (!$atomic_type instanceof TInt) {
$all_int = false;
break 2;
}

if ($atomic_type instanceof TLiteralInt) {
$min_bounds[] = $atomic_type->value;
$max_bounds[] = $atomic_type->value;
} elseif ($atomic_type instanceof TIntRange) {
$min_bounds[] = $atomic_type->min_bound;
$max_bounds[] = $atomic_type->max_bound;
} elseif (get_class($atomic_type) === TInt::class) {
$min_bounds[] = null;
$max_bounds[] = null;
} else {
throw new UnexpectedValueException('Unexpected type');
}
}
}
} else {
Expand Down Expand Up @@ -114,10 +136,21 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev
//if we're dealing with non-int elements, just combine them all together
$return_type = null;
foreach ($call_args as $arg) {
if ($array_arg_type = $nodeTypeProvider->getType($arg->value)) {
if ($arg_type = $nodeTypeProvider->getType($arg->value)) {
if ($arg->unpack) {
if ($arg_type->isSingle() && $arg_type->isArray()) {
$array_type = ArrayType::infer($arg_type->getSingleAtomic());
assert($array_type !== null);
$additional_type = $array_type->value;
} else {
$additional_type = Type::getMixed();
}
} else {
$additional_type = $arg_type;
}
$return_type = Type::combineUnionTypes(
$return_type,
$array_arg_type
$additional_type,
);
} else {
return Type::getMixed();
Expand Down
9 changes: 9 additions & 0 deletions src/Psalm/Internal/Type/ArrayType.php
Expand Up @@ -32,6 +32,15 @@ public function __construct(Union $key, Union $value, bool $is_list)
$this->is_list = $is_list;
}

/**
* @return (
* $type is TArrayKey ? self : (
* $type is TArray ? self : (
* $type is TList ? self : null
* )
* )
* )
*/
public static function infer(Atomic $type): ?self
{
if ($type instanceof TKeyedArray) {
Expand Down
30 changes: 30 additions & 0 deletions tests/FunctionCallTest.php
Expand Up @@ -1669,6 +1669,36 @@ function foo(DateTimeImmutable $fooDate): string
'$a' => 'DateTimeImmutable|float',
],
],
'maxUnpackArray' => [
'code' => '<?php
$files = [
__FILE__,
__FILE__,
__FILE__,
__FILE__,
];
$a = array_map("filemtime", $files);
$b = array_map(
function (string $file): int {
return filemtime($file);
},
$files,
);
$A = max(filemtime(__FILE__), ...$a);
$B = max(filemtime(__FILE__), ...$b);
echo date("c", $A), "\n", date("c", $B);
',
],
'maxUnpackArrayWithNonInt' => [
'code' => '<?php
$max = max(1, 2, ...[new DateTime(), 3, 4]);
',
'assertions' => [
'$max===' => '1|2|3|4|DateTime',
],
],
'strtolowerEmptiness' => [
'code' => '<?php
/** @param non-empty-string $s */
Expand Down
22 changes: 21 additions & 1 deletion tests/IntRangeTest.php
Expand Up @@ -609,6 +609,8 @@ function getInt(): int{return 0;}
$i = max($b, $c, $d);
$j = max($d, $e);
$k = max($e, 40);
$l = min($a, ...[$b, $c], $d);
$m = max(...[$a, ...[$b, $c]], $d);
',
'assertions' => [
'$f===' => 'int<min, -16>',
Expand All @@ -617,6 +619,8 @@ function getInt(): int{return 0;}
'$i===' => 'int<20, max>',
'$j===' => 'int<20, max>',
'$k===' => 'int<40, max>',
'$l===' => 'int<min, -16>',
'$m===' => 'int<20, max>',
],
],
'dontCrashOnFalsy' => [
Expand Down Expand Up @@ -813,7 +817,23 @@ function matches($key, int $expected): bool {
return true;
}'
]
],
'literalArrayUnpack' => [
'code' => '<?php
/** @var int<0, 5> */
$a = 2;
/** @var int<6, 10> */
$b = 9;
/**
* @param int<0, 5> $_a
* @param int<6, 10> $_b
*/
function foo(int $_a, int $_b): void {}
foo(...[$a, $b]);
',
],
];
}

Expand Down

0 comments on commit 97bd81c

Please sign in to comment.