Skip to content

Commit

Permalink
Merge pull request #82 from clue-labs/template-types-v3
Browse files Browse the repository at this point in the history
[3.x] Add template annotations
  • Loading branch information
WyriHaximus authored Nov 3, 2023
2 parents 037cbf2 + d1c9606 commit c41af0c
Show file tree
Hide file tree
Showing 12 changed files with 239 additions and 27 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ jobs:
- 7.4
- 7.3
- 7.2
- 7.1
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Async\await(…);

### await()

The `await(PromiseInterface $promise): mixed` function can be used to
The `await(PromiseInterface<T> $promise): T` function can be used to
block waiting for the given `$promise` to be fulfilled.

```php
Expand Down Expand Up @@ -94,7 +94,7 @@ try {

### coroutine()

The `coroutine(callable $function, mixed ...$args): PromiseInterface<mixed>` function can be used to
The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface<T>|T) $function, mixed ...$args): PromiseInterface<T>` function can be used to
execute a Generator-based coroutine to "await" promises.

```php
Expand Down Expand Up @@ -277,7 +277,7 @@ trigger at the earliest possible time in the future.

### parallel()

The `parallel(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
The `parallel(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:

```php
Expand Down Expand Up @@ -319,7 +319,7 @@ React\Async\parallel([

### series()

The `series(iterable<callable():PromiseInterface<mixed>> $tasks): PromiseInterface<array<mixed>>` function can be used
The `series(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:

```php
Expand Down Expand Up @@ -361,7 +361,7 @@ React\Async\series([

### waterfall()

The `waterfall(iterable<callable(mixed=):PromiseInterface<mixed>> $tasks): PromiseInterface<mixed>` function can be used
The `waterfall(iterable<callable(mixed=):PromiseInterface<T>> $tasks): PromiseInterface<T>` function can be used
like this:

```php
Expand Down
44 changes: 28 additions & 16 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@
* }
* ```
*
* @param PromiseInterface $promise
* @return mixed returns whatever the promise resolves to
* @template T
* @param PromiseInterface<T> $promise
* @return T returns whatever the promise resolves to
* @throws \Exception when the promise is rejected with an `Exception`
* @throws \Throwable when the promise is rejected with a `Throwable`
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
Expand Down Expand Up @@ -93,13 +94,14 @@ function ($error) use (&$exception, &$rejected, &$wait, &$loopStarted) {
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
if (!$exception instanceof \Throwable) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception)) // @phpstan-ignore-line
);
}

throw $exception;
}

/** @var T $resolved */
return $resolved;
}

Expand Down Expand Up @@ -296,9 +298,16 @@ function delay(float $seconds): void
* });
* ```
*
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
* @template T
* @template TYield
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<mixed>
* @return PromiseInterface<T>
* @since 3.0.0
*/
function coroutine(callable $function, ...$args): PromiseInterface
Expand All @@ -315,7 +324,7 @@ function coroutine(callable $function, ...$args): PromiseInterface

$promise = null;
$deferred = new Deferred(function () use (&$promise) {
/** @var ?PromiseInterface $promise */
/** @var ?PromiseInterface<T> $promise */
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
Expand All @@ -336,7 +345,6 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

/** @var mixed $promise */
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$next = null;
Expand All @@ -346,6 +354,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

/** @var PromiseInterface<TYield> $promise */
assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
Expand All @@ -364,12 +373,13 @@ function coroutine(callable $function, ...$args): PromiseInterface
}

/**
* @param iterable<callable():PromiseInterface<mixed>> $tasks
* @return PromiseInterface<array<mixed>>
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function parallel(iterable $tasks): PromiseInterface
{
/** @var array<int,PromiseInterface> $pending */
/** @var array<int,PromiseInterface<T>> $pending */
$pending = [];
$deferred = new Deferred(function () use (&$pending) {
foreach ($pending as $promise) {
Expand Down Expand Up @@ -424,14 +434,15 @@ function parallel(iterable $tasks): PromiseInterface
}

/**
* @param iterable<callable():PromiseInterface<mixed>> $tasks
* @return PromiseInterface<array<mixed>>
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function series(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand Down Expand Up @@ -478,14 +489,15 @@ function series(iterable $tasks): PromiseInterface
}

/**
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
* @return PromiseInterface<mixed>
* @template T
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
*/
function waterfall(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand Down
10 changes: 5 additions & 5 deletions tests/CoroutineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately
{
$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
return 42;
});
Expand Down Expand Up @@ -53,7 +53,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately()
{
$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
throw new \RuntimeException('Foo');
});
Expand Down Expand Up @@ -99,7 +99,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi

public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void
{
$promise = coroutine(function () {
$promise = coroutine(function () { // @phpstan-ignore-line
yield 42;
});

Expand Down Expand Up @@ -169,7 +169,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet

$promise = coroutine(function () {
if (false) { // @phpstan-ignore-line
yield;
yield resolve(null);
}
return 42;
});
Expand Down Expand Up @@ -249,7 +249,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYie

gc_collect_cycles();

$promise = coroutine(function () {
$promise = coroutine(function () { // @phpstan-ignore-line
yield 42;
});

Expand Down
3 changes: 3 additions & 0 deletions tests/ParallelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class ParallelTest extends TestCase
{
public function testParallelWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\parallel($tasks);
Expand Down
6 changes: 6 additions & 0 deletions tests/SeriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class SeriesTest extends TestCase
{
public function testSeriesWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\series($tasks);
Expand Down Expand Up @@ -152,6 +155,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
/** @var int */
public $called = 0;

/**
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
*/
public function getIterator(): \Iterator
{
while (true) { // @phpstan-ignore-line
Expand Down
6 changes: 6 additions & 0 deletions tests/WaterfallTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class WaterfallTest extends TestCase
{
public function testWaterfallWithoutTasks(): void
{
/**
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
*/
$tasks = array();

$promise = React\Async\waterfall($tasks);
Expand Down Expand Up @@ -166,6 +169,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
/** @var int */
public $called = 0;

/**
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
*/
public function getIterator(): \Iterator
{
while (true) { // @phpstan-ignore-line
Expand Down
18 changes: 18 additions & 0 deletions tests/types/await.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Promise\resolve;

assertType('bool', await(resolve(true)));

final class AwaitExampleUser
{
public string $name;

public function __construct(string $name) {
$this->name = $name;
}
}

assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);
60 changes: 60 additions & 0 deletions tests/types/coroutine.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Async\coroutine;
use function React\Promise\resolve;

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
return true;
}));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
return resolve(true);
}));

// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
// return (yield resolve(true));
// }));

assertType('React\Promise\PromiseInterface<int>', coroutine(static function () {
// $bool = yield resolve(true);
// assertType('bool', $bool);

return time();
}));

// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
// $bool = yield resolve(true);
// assertType('bool', $bool);

// return $bool;
// }));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
yield resolve(time());

return true;
}));

assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
for ($i = 0; $i <= 10; $i++) {
yield resolve($i);
}

return true;
}));

assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a): int { return $a; }, 42));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b): int { return $a + $b; }, 10, 32));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c): int { return $a + $b + $c; }, 10, 22, 10));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c, int $d): int { return $a + $b + $c + $d; }, 10, 22, 5, 5));
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c, int $d, int $e): int { return $a + $b + $c + $d + $e; }, 10, 12, 10, 5, 5));

assertType('bool', await(coroutine(static function () {
return true;
})));

// assertType('bool', await(coroutine(static function () {
// return (yield resolve(true));
// })));
33 changes: 33 additions & 0 deletions tests/types/parallel.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

use React\Promise\PromiseInterface;
use function PHPStan\Testing\assertType;
use function React\Async\await;
use function React\Async\parallel;
use function React\Promise\resolve;

assertType('React\Promise\PromiseInterface<array>', parallel([]));

assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
static function (): PromiseInterface { return resolve(true); },
static function (): PromiseInterface { return resolve(time()); },
static function (): PromiseInterface { return resolve(microtime(true)); },
]));

assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
static function (): bool { return true; },
static function (): int { return time(); },
static function (): float { return microtime(true); },
]));

assertType('array<bool|float|int>', await(parallel([
static function (): PromiseInterface { return resolve(true); },
static function (): PromiseInterface { return resolve(time()); },
static function (): PromiseInterface { return resolve(microtime(true)); },
])));

assertType('array<bool|float|int>', await(parallel([
static function (): bool { return true; },
static function (): int { return time(); },
static function (): float { return microtime(true); },
])));
Loading

0 comments on commit c41af0c

Please sign in to comment.