Skip to content

Commit c75b0ea

Browse files
committed
IteratorAggregate - read key and value type from generics if getIterator() doesn't have PHPDoc type
1 parent f69bd3e commit c75b0ea

File tree

5 files changed

+139
-4
lines changed

5 files changed

+139
-4
lines changed

src/Type/ObjectType.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -525,11 +525,14 @@ public function getIterableKeyType(): Type
525525
}
526526

527527
if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) {
528-
return RecursionGuard::run($this, static function () use ($classReflection): Type {
528+
$keyType = RecursionGuard::run($this, static function () use ($classReflection): Type {
529529
return ParametersAcceptorSelector::selectSingle(
530530
$classReflection->getNativeMethod('getIterator')->getVariants()
531531
)->getReturnType()->getIterableKeyType();
532532
});
533+
if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) {
534+
return $keyType;
535+
}
533536
}
534537

535538
if ($this->isInstanceOf(\Traversable::class)->yes()) {
@@ -558,11 +561,14 @@ public function getIterableValueType(): Type
558561
}
559562

560563
if ($this->isInstanceOf(\IteratorAggregate::class)->yes()) {
561-
return RecursionGuard::run($this, static function () use ($classReflection): Type {
564+
$valueType = RecursionGuard::run($this, static function () use ($classReflection): Type {
562565
return ParametersAcceptorSelector::selectSingle(
563566
$classReflection->getNativeMethod('getIterator')->getVariants()
564567
)->getReturnType()->getIterableValueType();
565568
});
569+
if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) {
570+
return $valueType;
571+
}
566572
}
567573

568574
if ($this->isInstanceOf(\Traversable::class)->yes()) {

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4893,13 +4893,13 @@ public function dataForeachObjectType(): array
48934893
],
48944894
[
48954895
__DIR__ . '/data/foreach/object-type.php',
4896-
'*ERROR*',
4896+
'mixed',
48974897
'$keyFromRecursiveAggregate',
48984898
"'insideThirdForeach'",
48994899
],
49004900
[
49014901
__DIR__ . '/data/foreach/object-type.php',
4902-
'*ERROR*',
4902+
'mixed',
49034903
'$valueFromRecursiveAggregate',
49044904
"'insideThirdForeach'",
49054905
],
@@ -10726,6 +10726,11 @@ public function dataBug4398(): array
1072610726
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4398.php');
1072710727
}
1072810728

10729+
public function dataBug4415(): array
10730+
{
10731+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4415.php');
10732+
}
10733+
1072910734
/**
1073010735
* @param string $file
1073110736
* @return array<string, mixed[]>
@@ -10937,6 +10942,7 @@ private function gatherAssertTypes(string $file): array
1093710942
* @dataProvider dataVarAboveDeclare
1093810943
* @dataProvider dataClosureReturnType
1093910944
* @dataProvider dataBug4398
10945+
* @dataProvider dataBug4415
1094010946
* @param string $assertType
1094110947
* @param string $file
1094210948
* @param mixed ...$args
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
namespace Bug4415;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
/**
8+
* @implements \IteratorAggregate<int, string>
9+
*/
10+
class Foo implements \IteratorAggregate
11+
{
12+
13+
public function getIterator(): \Iterator
14+
{
15+
16+
}
17+
18+
}
19+
20+
function (Foo $foo): void {
21+
foreach ($foo as $k => $v) {
22+
assertType('int', $k);
23+
assertType('string', $v);
24+
}
25+
};

tests/PHPStan/Rules/Methods/MissingMethodReturnTypehintRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,15 @@ public function testArrayTypehintWithoutNullInPhpDoc(): void
6363
$this->analyse([__DIR__ . '/../../Analyser/data/array-typehint-without-null-in-phpdoc.php'], []);
6464
}
6565

66+
public function testBug4415(): void
67+
{
68+
$this->analyse([__DIR__ . '/data/bug-4415.php'], [
69+
[
70+
'Method Bug4415Rule\CategoryCollection::getIterator() return type has no value type specified in iterable type Iterator.',
71+
76,
72+
"Consider adding something like <fg=cyan>Iterator<Foo></> to the PHPDoc.\nYou can turn off this check by setting <fg=cyan>checkMissingIterableValueType: false</> in your <fg=cyan>%configurationFile%</>.",
73+
],
74+
]);
75+
}
76+
6677
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace Bug4415Rule;
4+
5+
/**
6+
* @template T
7+
* @extends \IteratorAggregate<T>
8+
*/
9+
interface CollectionInterface extends \IteratorAggregate
10+
{
11+
/**
12+
* @param T $item
13+
*/
14+
public function has($item): bool;
15+
16+
/**
17+
* @return self<T>
18+
*/
19+
public function sort(): self;
20+
}
21+
22+
/**
23+
* @template T
24+
* @extends CollectionInterface<T>
25+
*/
26+
interface MutableCollectionInterface extends CollectionInterface
27+
{
28+
/**
29+
* @param T $item
30+
* @phpstan-return self<T>
31+
*/
32+
public function add($item): self;
33+
}
34+
35+
/**
36+
* @extends CollectionInterface<Category>
37+
*/
38+
interface CategoryCollectionInterface extends CollectionInterface
39+
{
40+
public function has($item): bool;
41+
42+
/**
43+
* @phpstan-return \Iterator<Category>
44+
*/
45+
public function getIterator(): \Iterator;
46+
}
47+
48+
/**
49+
* @extends MutableCollectionInterface<Category>
50+
*/
51+
interface MutableCategoryCollectionInterface extends CategoryCollectionInterface, MutableCollectionInterface
52+
{
53+
}
54+
55+
class CategoryCollection implements MutableCategoryCollectionInterface
56+
{
57+
/** @var array<Category> */
58+
private array $categories = [];
59+
60+
public function add($item): self
61+
{
62+
$this->categories[$item->getName()] = $item;
63+
return $this;
64+
}
65+
66+
public function has($item): bool
67+
{
68+
return isset($this->categories[$item->getName()]);
69+
}
70+
71+
public function sort(): self
72+
{
73+
return $this;
74+
}
75+
76+
public function getIterator(): \Iterator
77+
{
78+
return new \ArrayIterator($this->categories);
79+
}
80+
}
81+
82+
class Category {
83+
public function getName(): string
84+
{
85+
return '';
86+
}
87+
}

0 commit comments

Comments
 (0)