Skip to content

Commit c1f7aaf

Browse files
committed
Keep iterable key type and value type when subtracting from iterable
1 parent 3b109a7 commit c1f7aaf

File tree

6 files changed

+74
-5
lines changed

6 files changed

+74
-5
lines changed

src/Type/TypeCombinator.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\Type\Constant\ConstantFloatType;
1212
use PHPStan\Type\Constant\ConstantIntegerType;
1313
use PHPStan\Type\Constant\ConstantStringType;
14+
use PHPStan\Type\Generic\GenericObjectType;
1415
use PHPStan\Type\Generic\TemplateType;
1516

1617
class TypeCombinator
@@ -61,13 +62,17 @@ public static function remove(Type $fromType, Type $typeToRemove): Type
6162
return new ConstantBooleanType(!$typeToRemove->getValue());
6263
}
6364
} elseif ($fromType instanceof IterableType) {
64-
$traversableType = new ObjectType(\Traversable::class);
6565
$arrayType = new ArrayType(new MixedType(), new MixedType());
6666
if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) {
67-
return $traversableType;
67+
return new GenericObjectType(\Traversable::class, [
68+
$fromType->getIterableKeyType(),
69+
$fromType->getIterableValueType(),
70+
]);
6871
}
72+
73+
$traversableType = new ObjectType(\Traversable::class);
6974
if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) {
70-
return $arrayType;
75+
return new ArrayType($fromType->getIterableKeyType(), $fromType->getIterableValueType());
7176
}
7277
} elseif ($fromType instanceof IntegerRangeType) {
7378
$type = $fromType->tryRemove($typeToRemove);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10918,6 +10918,11 @@ public function dataBugInstanceOfClassString(): array
1091810918
return $this->gatherAssertTypes(__DIR__ . '/data/instanceof-class-string.php');
1091910919
}
1092010920

10921+
public function dataBug4498(): array
10922+
{
10923+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4498.php');
10924+
}
10925+
1092110926
/**
1092210927
* @param string $file
1092310928
* @return array<string, mixed[]>
@@ -11163,6 +11168,7 @@ private function gatherAssertTypes(string $file): array
1116311168
* @dataProvider dataBug3321
1116411169
* @dataProvider dataBug3769
1116511170
* @dataProvider dataBugInstanceOfClassString
11171+
* @dataProvider dataBug4498
1116611172
* @param string $assertType
1116711173
* @param string $file
1116811174
* @param mixed ...$args

tests/PHPStan/Analyser/data/bug-1233.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public function toArray($value): array
1717
assertType('mixed~array', $value);
1818

1919
if (is_iterable($value)) {
20-
assertType('Traversable', $value);
20+
assertType('Traversable<mixed, mixed>', $value);
2121
return iterator_to_array($value);
2222
}
2323

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Bug4498;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param iterable<TKey, TValue> $iterable
12+
*
13+
* @return iterable<TKey, TValue>
14+
*
15+
* @template TKey
16+
* @template TValue
17+
*/
18+
public function fcn(iterable $iterable): iterable
19+
{
20+
if ($iterable instanceof \Traversable) {
21+
assertType('iterable<TKey (method Bug4498\Foo::fcn(), argument), TValue (method Bug4498\Foo::fcn(), argument)>&Traversable', $iterable);
22+
return $iterable;
23+
}
24+
25+
assertType('array<TKey (method Bug4498\Foo::fcn(), argument), TValue (method Bug4498\Foo::fcn(), argument)>', $iterable);
26+
27+
return $iterable;
28+
}
29+
30+
/**
31+
* @param iterable<TKey, TValue> $iterable
32+
*
33+
* @return iterable<TKey, TValue>
34+
*
35+
* @template TKey
36+
* @template TValue
37+
*/
38+
public function bar(iterable $iterable): iterable
39+
{
40+
if (is_array($iterable)) {
41+
assertType('array<TKey (method Bug4498\Foo::bar(), argument), TValue (method Bug4498\Foo::bar(), argument)>', $iterable);
42+
return $iterable;
43+
}
44+
45+
assertType('Traversable<TKey (method Bug4498\Foo::bar(), argument), TValue (method Bug4498\Foo::bar(), argument)>', $iterable);
46+
47+
return $iterable;
48+
}
49+
50+
}

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1862,4 +1862,12 @@ public function testBug3321(): void
18621862
$this->analyse([__DIR__ . '/../../Analyser/data/bug-3321.php'], []);
18631863
}
18641864

1865+
public function testBug4498(): void
1866+
{
1867+
$this->checkThisOnly = false;
1868+
$this->checkNullables = true;
1869+
$this->checkUnionTypes = true;
1870+
$this->analyse([__DIR__ . '/../../Analyser/data/bug-4498.php'], []);
1871+
}
1872+
18651873
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3176,7 +3176,7 @@ public function dataRemove(): array
31763176
new IterableType(new MixedType(), new MixedType()),
31773177
new ArrayType(new MixedType(), new MixedType()),
31783178
ObjectType::class,
3179-
'Traversable',
3179+
'Traversable<mixed, mixed>',
31803180
],
31813181
[
31823182
new IterableType(new MixedType(), new MixedType()),

0 commit comments

Comments
 (0)