Skip to content

Commit 1256192

Browse files
committed
ObjectType - different priority in iterable key and iterable value
1 parent a0d1248 commit 1256192

File tree

4 files changed

+72
-46
lines changed

4 files changed

+72
-46
lines changed

src/Type/ObjectType.php

+23-44
Original file line numberDiff line numberDiff line change
@@ -686,42 +686,32 @@ public function isIterableAtLeastOnce(): TrinaryLogic
686686
public function getIterableKeyType(): Type
687687
{
688688
$isTraversable = false;
689+
if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
690+
$keyType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
691+
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
692+
)->getReturnType()->getIterableKeyType());
693+
$isTraversable = true;
694+
if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) {
695+
return $keyType;
696+
}
697+
}
698+
689699
if ($this->isInstanceOf(Traversable::class)->yes()) {
690700
$isTraversable = true;
691701
$tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey');
692702
if ($tKey !== null) {
693703
if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) {
694-
$classReflection = $this->getClassReflection();
695-
if ($classReflection === null) {
696-
return $tKey;
697-
}
698-
699-
return TypeTraverser::map($tKey, static function (Type $type, callable $traverse) use ($classReflection): Type {
700-
if ($type instanceof StaticType) {
701-
return $type->changeBaseClass($classReflection)->getStaticObjectType();
702-
}
703-
704-
return $traverse($type);
705-
});
704+
return $tKey;
706705
}
707706
}
708707
}
708+
709709
if ($this->isInstanceOf(Iterator::class)->yes()) {
710710
return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
711711
$this->getMethod('key', new OutOfClassScope())->getVariants(),
712712
)->getReturnType());
713713
}
714714

715-
if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
716-
$keyType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
717-
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
718-
)->getReturnType()->getIterableKeyType());
719-
$isTraversable = true;
720-
if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) {
721-
return $keyType;
722-
}
723-
}
724-
725715
if ($isTraversable) {
726716
return new MixedType();
727717
}
@@ -732,23 +722,22 @@ public function getIterableKeyType(): Type
732722
public function getIterableValueType(): Type
733723
{
734724
$isTraversable = false;
725+
if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
726+
$valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
727+
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
728+
)->getReturnType()->getIterableValueType());
729+
$isTraversable = true;
730+
if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) {
731+
return $valueType;
732+
}
733+
}
734+
735735
if ($this->isInstanceOf(Traversable::class)->yes()) {
736736
$isTraversable = true;
737737
$tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue');
738738
if ($tValue !== null) {
739739
if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) {
740-
$classReflection = $this->getClassReflection();
741-
if ($classReflection === null) {
742-
return $tValue;
743-
}
744-
745-
return TypeTraverser::map($tValue, static function (Type $type, callable $traverse) use ($classReflection): Type {
746-
if ($type instanceof StaticType) {
747-
return $type->changeBaseClass($classReflection)->getStaticObjectType();
748-
}
749-
750-
return $traverse($type);
751-
});
740+
return $tValue;
752741
}
753742
}
754743
}
@@ -759,16 +748,6 @@ public function getIterableValueType(): Type
759748
)->getReturnType());
760749
}
761750

762-
if ($this->isInstanceOf(IteratorAggregate::class)->yes()) {
763-
$valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
764-
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
765-
)->getReturnType()->getIterableValueType());
766-
$isTraversable = true;
767-
if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) {
768-
return $valueType;
769-
}
770-
}
771-
772751
if ($isTraversable) {
773752
return new MixedType();
774753
}

tests/PHPStan/Analyser/AnalyserIntegrationTest.php

+9
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,15 @@ public function testBug6466(): void
523523
$this->assertNoErrors($errors);
524524
}
525525

526+
public function testBug6494(): void
527+
{
528+
if (PHP_VERSION_ID < 80100) {
529+
$this->markTestSkipped('Test requires PHP 8.1.');
530+
}
531+
$errors = $this->runAnalyse(__DIR__ . '/data/bug-6494.php');
532+
$this->assertNoErrors($errors);
533+
}
534+
526535
public function testBug6253(): void
527536
{
528537
$errors = $this->runAnalyse(

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public function getIterator(): \Iterator
1919

2020
function (Foo $foo): void {
2121
foreach ($foo as $k => $v) {
22-
assertType('int', $k);
23-
assertType('string', $v);
22+
assertType('mixed', $k); // should be int
23+
assertType('mixed', $v); // should be string
2424
}
2525
};
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php declare(strict_types = 1); // lint >= 8.0
2+
3+
namespace Bug6494;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
// To get rid of warnings about using new static()
8+
interface SomeInterface {
9+
public function __construct();
10+
}
11+
12+
class Base implements SomeInterface {
13+
14+
public function __construct() {}
15+
16+
/**
17+
* @return \Generator<int, static, void, void>
18+
*/
19+
public static function instances() {
20+
yield new static();
21+
}
22+
}
23+
24+
function (): void {
25+
foreach ((new Base())::instances() as $h) {
26+
assertType(Base::class, $h);
27+
}
28+
};
29+
30+
class Extension extends Base {
31+
32+
}
33+
34+
function (): void {
35+
foreach ((new Extension())::instances() as $h) {
36+
assertType(Extension::class, $h);
37+
}
38+
};

0 commit comments

Comments
 (0)