Skip to content

Commit

Permalink
next() dynamic return type extension
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Jul 3, 2021
1 parent d675602 commit 599f59b
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 0 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -953,6 +953,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayNextDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\ArrayPopFunctionReturnTypeExtension
tags:
Expand Down
38 changes: 38 additions & 0 deletions src/Type/Php/ArrayNextDynamicReturnTypeExtension.php
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

class ArrayNextDynamicReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'next';
}

public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
{
if (!isset($functionCall->args[0])) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}

$argType = $scope->getType($functionCall->args[0]->value);
$iterableAtLeastOnce = $argType->isIterableAtLeastOnce();
if ($iterableAtLeastOnce->no()) {
return new ConstantBooleanType(false);
}

$valueType = $argType->getIterableValueType();

return TypeCombinator::union($valueType, new ConstantBooleanType(false));
}

}
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -431,6 +431,7 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/closure-types.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5219.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/strval.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-next.php');
}

/**
Expand Down
77 changes: 77 additions & 0 deletions tests/PHPStan/Analyser/data/array-next.php
@@ -0,0 +1,77 @@
<?php

namespace ArrayNext;

use function PHPStan\Testing\assertType;

class Foo
{

public function doFoo()
{
$array = [];
assertType('false', next($array));
}

/**
* @param int[] $a
*/
public function doBar(array $a)
{
assertType('int|false', next($a));
}

/**
* @param non-empty-array<int, string> $a
*/
public function doBaz(array $a)
{
assertType('string|false', next($a));
}

}

interface HttpClientPoolItem
{
public function isDisabled(): bool;
}

final class RoundRobinClientPool
{
/**
* @var HttpClientPoolItem[]
*/
protected $clientPool = [];

protected function chooseHttpClient(): HttpClientPoolItem
{
$last = current($this->clientPool);
assertType(HttpClientPoolItem::class . '|false', $last);

do {
$client = next($this->clientPool);
assertType(HttpClientPoolItem::class . '|false', $client);

if (false === $client) {
$client = reset($this->clientPool);
assertType(HttpClientPoolItem::class . '|false', $client);

if (false === $client) {
throw new \Exception();
}

assertType(HttpClientPoolItem::class, $client);
}

assertType(HttpClientPoolItem::class, $client);

// Case when there is only one and the last one has been disabled
if ($last === $client) {
assertType(HttpClientPoolItem::class, $client);
throw new \Exception();
}
} while ($client->isDisabled());

return $client;
}
}
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Expand Up @@ -1971,4 +1971,12 @@ public function testBug4083(): void
$this->analyse([__DIR__ . '/data/bug-4083.php'], []);
}

public function testBug5253(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-5253.php'], []);
}

}
40 changes: 40 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-5253.php
@@ -0,0 +1,40 @@
<?php

namespace Bug5253;

interface HttpClientPoolItem
{
public function isDisabled(): bool;
}

final class RoundRobinClientPool
{
/**
* @var HttpClientPoolItem[]
*/
protected $clientPool = [];

protected function chooseHttpClient(): HttpClientPoolItem
{
$last = current($this->clientPool);

do {
$client = next($this->clientPool);

if (false === $client) {
$client = reset($this->clientPool);

if (false === $client) {
throw new \Exception();
}
}

// Case when there is only one and the last one has been disabled
if ($last === $client && $client->isDisabled()) {
throw new \Exception();
}
} while ($client->isDisabled());

return $client;
}
}

0 comments on commit 599f59b

Please sign in to comment.