Skip to content

Commit

Permalink
Fix iterable key and value type - prefer generic type variables inste…
Browse files Browse the repository at this point in the history
…ad of key()/current() typehints
  • Loading branch information
ondrejmirtes committed Jan 25, 2022
1 parent 6dc7e3a commit a2acf64
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 16 deletions.
62 changes: 46 additions & 16 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -680,11 +680,27 @@ public function isIterableAtLeastOnce(): TrinaryLogic

public function getIterableKeyType(): Type
{
$classReflection = $this->getClassReflection();
if ($classReflection === null) {
return new ErrorType();
$isTraversable = false;
if ($this->isInstanceOf(Traversable::class)->yes()) {
$isTraversable = true;
$tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey');
if ($tKey !== null) {
if (!$tKey instanceof MixedType || $tKey->isExplicitMixed()) {
$classReflection = $this->getClassReflection();
if ($classReflection === null) {
return $tKey;
}

return TypeTraverser::map($tKey, static function (Type $type, callable $traverse) use ($classReflection): Type {
if ($type instanceof StaticType) {
return $type->changeBaseClass($classReflection)->getStaticObjectType();
}

return $traverse($type);
});
}
}
}

if ($this->isInstanceOf(Iterator::class)->yes()) {
return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
$this->getMethod('key', new OutOfClassScope())->getVariants(),
Expand All @@ -695,17 +711,13 @@ public function getIterableKeyType(): Type
$keyType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
)->getReturnType()->getIterableKeyType());
$isTraversable = true;
if (!$keyType instanceof MixedType || $keyType->isExplicitMixed()) {
return $keyType;
}
}

if ($this->isInstanceOf(Traversable::class)->yes()) {
$tKey = GenericTypeVariableResolver::getType($this, Traversable::class, 'TKey');
if ($tKey !== null) {
return $tKey;
}

if ($isTraversable) {
return new MixedType();
}

Expand All @@ -714,6 +726,28 @@ public function getIterableKeyType(): Type

public function getIterableValueType(): Type
{
$isTraversable = false;
if ($this->isInstanceOf(Traversable::class)->yes()) {
$isTraversable = true;
$tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue');
if ($tValue !== null) {
if (!$tValue instanceof MixedType || $tValue->isExplicitMixed()) {
$classReflection = $this->getClassReflection();
if ($classReflection === null) {
return $tValue;
}

return TypeTraverser::map($tValue, static function (Type $type, callable $traverse) use ($classReflection): Type {
if ($type instanceof StaticType) {
return $type->changeBaseClass($classReflection)->getStaticObjectType();
}

return $traverse($type);
});
}
}
}

if ($this->isInstanceOf(Iterator::class)->yes()) {
return RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
$this->getMethod('current', new OutOfClassScope())->getVariants(),
Expand All @@ -724,17 +758,13 @@ public function getIterableValueType(): Type
$valueType = RecursionGuard::run($this, fn (): Type => ParametersAcceptorSelector::selectSingle(
$this->getMethod('getIterator', new OutOfClassScope())->getVariants(),
)->getReturnType()->getIterableValueType());
$isTraversable = true;
if (!$valueType instanceof MixedType || $valueType->isExplicitMixed()) {
return $valueType;
}
}

if ($this->isInstanceOf(Traversable::class)->yes()) {
$tValue = GenericTypeVariableResolver::getType($this, Traversable::class, 'TValue');
if ($tValue !== null) {
return $tValue;
}

if ($isTraversable) {
return new MixedType();
}

Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6404.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6399.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php');
}

/**
Expand Down
124 changes: 124 additions & 0 deletions tests/PHPStan/Analyser/data/bug-5817.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php

declare(strict_types=1);

namespace Bug5817;

use ArrayAccess;
use Countable;
use DateTimeInterface;
use Iterator;
use JsonSerializable;

use function count;
use function current;
use function key;
use function next;
use function PHPStan\Testing\assertType;

/**
* @implements ArrayAccess<int, DateTimeInterface>
* @implements Iterator<int, DateTimeInterface>
*/
class MyContainer implements
ArrayAccess,
Countable,
Iterator,
JsonSerializable
{
/** @var array<int, DateTimeInterface> */
protected array $items = [];

public function add(DateTimeInterface $item, int $offset = null): self
{
$this->offsetSet($offset, $item);
return $this;
}

public function count(): int
{
return count($this->items);
}

/** @return DateTimeInterface|false */
public function current()
{
return current($this->items);
}

/** @return DateTimeInterface|false */
public function next()
{
return next($this->items);
}

/** @return int|null */
public function key(): ?int
{
return key($this->items);
}

public function valid(): bool
{
return $this->key() !== null;
}

/** @return DateTimeInterface|false */
public function rewind()
{
return reset($this->items);
}

/** @param mixed $offset */
public function offsetExists($offset): bool
{
return isset($this->items[$offset]);
}

/** @param mixed $offset */
public function offsetGet($offset): ?DateTimeInterface
{
return $this->items[$offset] ?? null;
}

/**
* @param mixed $offset
* @param mixed $value
*/
public function offsetSet($offset, $value): void
{
assert($value instanceof DateTimeInterface);
if ($offset === null) { // append
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}

/** @param mixed $offset */
public function offsetUnset($offset): void
{
unset($this->items[$offset]);
}

/** @return DateTimeInterface[] */
public function jsonSerialize(): array
{
return $this->items;
}
}

class Foo
{

public function doFoo()
{
$container = (new MyContainer())->add(new \DateTimeImmutable());

foreach ($container as $k => $item) {
assertType('int', $k);
assertType(DateTimeInterface::class, $item);
}
}

}

0 comments on commit a2acf64

Please sign in to comment.