1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Fix #710 - treat keyed intersections of iterators properly

This commit is contained in:
Matthew Brown 2019-05-29 19:58:54 -04:00
parent 8955120bd1
commit 962d8f30a1
6 changed files with 109 additions and 10 deletions

View File

@ -408,19 +408,62 @@ class ForeachAnalyzer
$has_valid_iterator = true;
$value_type = Type::getMixed();
} elseif ($iterator_atomic_type instanceof Type\Atomic\TIterable) {
$value_type_part = $iterator_atomic_type->type_params[1];
$key_type_part = $iterator_atomic_type->type_params[0];
if ($iterator_atomic_type->extra_types) {
$iterator_atomic_type_copy = clone $iterator_atomic_type;
$iterator_atomic_type_copy->extra_types = [];
$iterator_atomic_types = [$iterator_atomic_type_copy];
$iterator_atomic_types = array_merge(
$iterator_atomic_types,
$iterator_atomic_type->extra_types
);
} else {
$iterator_atomic_types = [$iterator_atomic_type];
}
$intersection_value_type = null;
$intersection_key_type = null;
foreach ($iterator_atomic_types as $iat) {
if (!$iat instanceof Type\Atomic\TIterable) {
continue;
}
$value_type_part = $iat->type_params[1];
$key_type_part = $iat->type_params[0];
if (!$intersection_value_type) {
$intersection_value_type = $value_type_part;
} else {
$intersection_value_type = Type::intersectUnionTypes(
$intersection_value_type,
$value_type_part
);
}
if (!$intersection_key_type) {
$intersection_key_type = $key_type_part;
} else {
$intersection_key_type = Type::intersectUnionTypes(
$intersection_key_type,
$key_type_part
);
}
}
if (!$intersection_value_type || !$intersection_key_type) {
throw new \UnexpectedValueException('Should not happen');
}
if (!$value_type) {
$value_type = $value_type_part;
$value_type = $intersection_value_type;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
$value_type = Type::combineUnionTypes($value_type, $intersection_value_type);
}
if (!$key_type) {
$key_type = $key_type_part;
$key_type = $intersection_key_type;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
$key_type = Type::combineUnionTypes($key_type, $intersection_key_type);
}
$has_valid_iterator = true;

View File

@ -908,9 +908,10 @@ class Populator
if (isset($atomic_types['array']) && count($atomic_types) > 1 && !isset($atomic_types['null'])) {
$iterator_name = null;
$generic_params = null;
$iterator_key = null;
try {
foreach ($atomic_types as $type) {
foreach ($atomic_types as $type_key => $type) {
if ($type instanceof Type\Atomic\TIterable
|| ($type instanceof Type\Atomic\TNamedObject
&& (!$type->from_docblock || $is_property)
@ -927,6 +928,7 @@ class Populator
))
) {
$iterator_name = $type->value;
$iterator_key = $type_key;
} elseif ($type instanceof Type\Atomic\TArray) {
$generic_params = $type->type_params;
}
@ -935,7 +937,7 @@ class Populator
// ignore class-not-found issues
}
if ($iterator_name && $generic_params) {
if ($iterator_name && $iterator_key && $generic_params) {
if ($iterator_name === 'iterable') {
$generic_iterator = new Type\Atomic\TIterable($generic_params);
} else {
@ -947,7 +949,7 @@ class Populator
}
$candidate->removeType('array');
$candidate->removeType($iterator_name);
$candidate->removeType($iterator_key);
$candidate->addType($generic_iterator);
}
}

View File

@ -402,7 +402,11 @@ abstract class Type
);
}
$keyed_intersection_types[$intersection_type->getKey()] = $intersection_type;
$keyed_intersection_types[
$intersection_type instanceof TIterable
? $intersection_type->getId()
: $intersection_type->getKey()
] = $intersection_type;
}
$first_type = array_shift($keyed_intersection_types);

View File

@ -39,6 +39,27 @@ class TIterable extends Atomic
return 'iterable';
}
public function getId()
{
$s = '';
foreach ($this->type_params as $type_param) {
$s .= $type_param->getKey() . ', ';
}
$extra_types = '';
if ($this->extra_types) {
$extra_types = '&' . implode('&', $this->extra_types);
}
return $this->value . '<' . substr($s, 0, -2) . '>' . $extra_types;
}
public function __toString()
{
return $this->getId();
}
/**
* @param string|null $namespace
* @param array<string> $aliased_classes

View File

@ -618,6 +618,27 @@ class InterfaceTest extends TestCase
foo($i);
}'
],
'intersectIterators' => [
'<?php
class A {} function takesA(A $p): void {}
class B {} function takesB(B $p): void {}
/** @psalm-param iterable<A>&iterable<B> $i */
function takesIntersectionOfIterables(iterable $i): void {
foreach ($i as $c) {
takesA($c);
takesB($c);
}
}
/** @psalm-param iterable<A&B> $i */
function takesIterableOfIntersections(iterable $i): void {
foreach ($i as $c) {
takesA($c);
takesB($c);
}
}'
],
];
}

View File

@ -203,6 +203,14 @@ class TypeParseTest extends TestCase
$this->assertSame('Countable&iterable<mixed, int>&I', (string) Type::parseString('Countable&iterable<int>&I'));
}
/**
* @return void
*/
public function testIntersectionOfIterables()
{
$this->assertSame('iterable<mixed, A>&iterable<mixed, B>', (string) Type::parseString('iterable<A>&iterable<B>'));
}
/**
* @return void
*/