Skip to content

Commit

Permalink
Merge pull request reactphp#40 from WyriHaximus-labs/4.x-add-template…
Browse files Browse the repository at this point in the history
…-annotations

[4.x] Add template annotations
  • Loading branch information
WyriHaximus authored Oct 27, 2023
2 parents 307684c + 643316a commit 8cc37cc
Show file tree
Hide file tree
Showing 14 changed files with 288 additions and 34 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Async\await(…);

### async()

The `async(callable $function): callable` function can be used to
The `async(callable():(PromiseInterface<T>|T) $function): (callable():PromiseInterface<T>)` function can be used to
return an async function for a function that uses [`await()`](#await) internally.

This function is specifically designed to complement the [`await()` function](#await).
Expand Down Expand Up @@ -226,7 +226,7 @@ await($promise);

### 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 @@ -278,7 +278,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 @@ -498,7 +498,7 @@ Loop::addTimer(2.0, function () use ($promise): void {

### 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 @@ -540,7 +540,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 @@ -582,7 +582,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
19 changes: 15 additions & 4 deletions src/FiberMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@

/**
* @internal
*
* @template T
*/
final class FiberMap
{
/** @var array<int,bool> */
private static array $status = [];

/** @var array<int,PromiseInterface> */
/** @var array<int,PromiseInterface<T>> */
private static array $map = [];

/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
Expand All @@ -27,19 +29,28 @@ public static function cancel(\Fiber $fiber): void
self::$status[\spl_object_id($fiber)] = true;
}

/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @param PromiseInterface<T> $promise
*/
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
{
self::$map[\spl_object_id($fiber)] = $promise;
}

/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @param PromiseInterface<T> $promise
*/
public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void
{
unset(self::$map[\spl_object_id($fiber)]);
}

/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @return ?PromiseInterface<T>
*/
public static function getPromise(\Fiber $fiber): ?PromiseInterface
{
return self::$map[\spl_object_id($fiber)] ?? null;
Expand Down
56 changes: 38 additions & 18 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,14 @@
* await($promise);
* ```
*
* @param callable $function
* @return callable(mixed ...): PromiseInterface<mixed>
* @template T
* @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): (PromiseInterface<T>|T) $function
* @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface<T>
* @since 4.0.0
* @see coroutine()
*/
Expand Down Expand Up @@ -268,8 +274,9 @@ function async(callable $function): callable
* }
* ```
*
* @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 All @@ -279,6 +286,8 @@ function await(PromiseInterface $promise): mixed
$fiber = null;
$resolved = false;
$rejected = false;

/** @var T $resolvedValue */
$resolvedValue = null;
$rejectedThrowable = null;
$lowLevelFiber = \Fiber::getCurrent();
Expand All @@ -292,6 +301,7 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
if ($fiber === null) {
$resolved = true;
/** @var T $resolvedValue */
$resolvedValue = $value;
return;
}
Expand All @@ -305,7 +315,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL

if (!$throwable instanceof \Throwable) {
$throwable = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable))
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */
);

// avoid garbage references by replacing all closures in call stack.
Expand Down Expand Up @@ -592,9 +602,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, mixed ...$args): PromiseInterface
Expand All @@ -611,7 +628,7 @@ function coroutine(callable $function, mixed ...$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 @@ -632,7 +649,6 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
return;
}

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

/** @var PromiseInterface<TYield> $promise */
assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
Expand All @@ -660,12 +677,13 @@ function coroutine(callable $function, mixed ...$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 @@ -720,14 +738,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 @@ -774,14 +793,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
2 changes: 1 addition & 1 deletion tests/AwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ public function testRejectedPromisesShouldBeDetached(callable $await): void
})());
}

/** @return iterable<string,list<callable(PromiseInterface): mixed>> */
/** @return iterable<string,list<callable(PromiseInterface<mixed>): mixed>> */
public function provideAwaiters(): iterable
{
yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];
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 @@ -151,6 +154,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
$tasks = new class() implements \IteratorAggregate {
public int $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 @@ -165,6 +168,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
$tasks = new class() implements \IteratorAggregate {
public int $called = 0;

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

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

assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => true)());
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): PromiseInterface => resolve(true))());
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => await(resolve(true)))());

assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a): int => $a)(42));
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b): int => $a + $b)(10, 32));
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c): int => $a + $b + $c)(10, 22, 10));
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c, int $d): int => $a + $b + $c + $d)(10, 22, 5, 5));
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c, int $d, int $e): int => $a + $b + $c + $d + $e)(10, 12, 10, 5, 5));
Loading

0 comments on commit 8cc37cc

Please sign in to comment.