Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4.x] Add template annotations #40

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"640K ought to be enough for anybody."

See phpstan/phpstan#8214 for longer-term solution once supported in PHPStan.

* @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