diff --git a/README.md b/README.md index 6e71f49..d8dd55c 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Async\await(…); ### async() -The `async(callable $function): callable` function can be used to +The `async(callable():(PromiseInterface|T) $function): (callable():PromiseInterface)` 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). @@ -226,7 +226,7 @@ await($promise); ### await() -The `await(PromiseInterface $promise): mixed` function can be used to +The `await(PromiseInterface $promise): T` function can be used to block waiting for the given `$promise` to be fulfilled. ```php @@ -278,7 +278,7 @@ try { ### coroutine() -The `coroutine(callable $function, mixed ...$args): PromiseInterface` function can be used to +The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface|T) $function, mixed ...$args): PromiseInterface` function can be used to execute a Generator-based coroutine to "await" promises. ```php @@ -498,7 +498,7 @@ Loop::addTimer(2.0, function () use ($promise): void { ### parallel() -The `parallel(iterable> $tasks): PromiseInterface>` function can be used +The `parallel(iterable> $tasks): PromiseInterface>` function can be used like this: ```php @@ -540,7 +540,7 @@ React\Async\parallel([ ### series() -The `series(iterable> $tasks): PromiseInterface>` function can be used +The `series(iterable> $tasks): PromiseInterface>` function can be used like this: ```php @@ -582,7 +582,7 @@ React\Async\series([ ### waterfall() -The `waterfall(iterable> $tasks): PromiseInterface` function can be used +The `waterfall(iterable> $tasks): PromiseInterface` function can be used like this: ```php diff --git a/src/FiberMap.php b/src/FiberMap.php index 0648788..f843a2d 100644 --- a/src/FiberMap.php +++ b/src/FiberMap.php @@ -6,13 +6,15 @@ /** * @internal + * + * @template T */ final class FiberMap { /** @var array */ private static array $status = []; - /** @var array */ + /** @var array> */ private static array $map = []; /** @param \Fiber $fiber */ @@ -27,19 +29,28 @@ public static function cancel(\Fiber $fiber): void self::$status[\spl_object_id($fiber)] = true; } - /** @param \Fiber $fiber */ + /** + * @param \Fiber $fiber + * @param PromiseInterface $promise + */ public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void { self::$map[\spl_object_id($fiber)] = $promise; } - /** @param \Fiber $fiber */ + /** + * @param \Fiber $fiber + * @param PromiseInterface $promise + */ public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void { unset(self::$map[\spl_object_id($fiber)]); } - /** @param \Fiber $fiber */ + /** + * @param \Fiber $fiber + * @return ?PromiseInterface + */ public static function getPromise(\Fiber $fiber): ?PromiseInterface { return self::$map[\spl_object_id($fiber)] ?? null; diff --git a/src/functions.php b/src/functions.php index 5a02406..6c27936 100644 --- a/src/functions.php +++ b/src/functions.php @@ -176,8 +176,14 @@ * await($promise); * ``` * - * @param callable $function - * @return callable(mixed ...): PromiseInterface + * @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) $function + * @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface * @since 4.0.0 * @see coroutine() */ @@ -268,8 +274,9 @@ function async(callable $function): callable * } * ``` * - * @param PromiseInterface $promise - * @return mixed returns whatever the promise resolves to + * @template T + * @param PromiseInterface $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) @@ -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(); @@ -292,6 +301,7 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe /** @var ?\Fiber $fiber */ if ($fiber === null) { $resolved = true; + /** @var T $resolvedValue */ $resolvedValue = $value; return; } @@ -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. @@ -592,9 +602,16 @@ function delay(float $seconds): void * }); * ``` * - * @param callable(mixed ...$args):(\Generator|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, TYield, PromiseInterface|T>|PromiseInterface|T) $function * @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is - * @return PromiseInterface + * @return PromiseInterface * @since 3.0.0 */ function coroutine(callable $function, mixed ...$args): PromiseInterface @@ -611,7 +628,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface $promise = null; $deferred = new Deferred(function () use (&$promise) { - /** @var ?PromiseInterface $promise */ + /** @var ?PromiseInterface $promise */ if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { $promise->cancel(); } @@ -632,7 +649,6 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface return; } - /** @var mixed $promise */ $promise = $generator->current(); if (!$promise instanceof PromiseInterface) { $next = null; @@ -642,6 +658,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface return; } + /** @var PromiseInterface $promise */ assert($next instanceof \Closure); $promise->then(function ($value) use ($generator, $next) { $generator->send($value); @@ -660,12 +677,13 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface } /** - * @param iterable> $tasks - * @return PromiseInterface> + * @template T + * @param iterable|T)> $tasks + * @return PromiseInterface> */ function parallel(iterable $tasks): PromiseInterface { - /** @var array $pending */ + /** @var array> $pending */ $pending = []; $deferred = new Deferred(function () use (&$pending) { foreach ($pending as $promise) { @@ -720,14 +738,15 @@ function parallel(iterable $tasks): PromiseInterface } /** - * @param iterable> $tasks - * @return PromiseInterface> + * @template T + * @param iterable|T)> $tasks + * @return PromiseInterface> */ function series(iterable $tasks): PromiseInterface { $pending = null; $deferred = new Deferred(function () use (&$pending) { - /** @var ?PromiseInterface $pending */ + /** @var ?PromiseInterface $pending */ if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { $pending->cancel(); } @@ -774,14 +793,15 @@ function series(iterable $tasks): PromiseInterface } /** - * @param iterable<(callable():PromiseInterface)|(callable(mixed):PromiseInterface)> $tasks - * @return PromiseInterface + * @template T + * @param iterable<(callable():(PromiseInterface|T))|(callable(mixed):(PromiseInterface|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 $pending */ if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { $pending->cancel(); } diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 3158a1b..25e269b 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -413,7 +413,7 @@ public function testRejectedPromisesShouldBeDetached(callable $await): void })()); } - /** @return iterable> */ + /** @return iterable): mixed>> */ public function provideAwaiters(): iterable { yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)]; diff --git a/tests/CoroutineTest.php b/tests/CoroutineTest.php index 2c674c5..1df4cdc 100644 --- a/tests/CoroutineTest.php +++ b/tests/CoroutineTest.php @@ -22,7 +22,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately { $promise = coroutine(function () { if (false) { // @phpstan-ignore-line - yield; + yield resolve(null); } return 42; }); @@ -53,7 +53,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately() { $promise = coroutine(function () { if (false) { // @phpstan-ignore-line - yield; + yield resolve(null); } throw new \RuntimeException('Foo'); }); @@ -99,7 +99,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void { - $promise = coroutine(function () { + $promise = coroutine(function () { // @phpstan-ignore-line yield 42; }); @@ -169,7 +169,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet $promise = coroutine(function () { if (false) { // @phpstan-ignore-line - yield; + yield resolve(null); } return 42; }); @@ -249,7 +249,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYie gc_collect_cycles(); - $promise = coroutine(function () { + $promise = coroutine(function () { // @phpstan-ignore-line yield 42; }); diff --git a/tests/ParallelTest.php b/tests/ParallelTest.php index 37b1e10..ad24589 100644 --- a/tests/ParallelTest.php +++ b/tests/ParallelTest.php @@ -12,6 +12,9 @@ class ParallelTest extends TestCase { public function testParallelWithoutTasks(): void { + /** + * @var array> $tasks + */ $tasks = array(); $promise = React\Async\parallel($tasks); diff --git a/tests/SeriesTest.php b/tests/SeriesTest.php index 9b20815..69cafd5 100644 --- a/tests/SeriesTest.php +++ b/tests/SeriesTest.php @@ -12,6 +12,9 @@ class SeriesTest extends TestCase { public function testSeriesWithoutTasks(): void { + /** + * @var array> $tasks + */ $tasks = array(); $promise = React\Async\series($tasks); @@ -151,6 +154,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe $tasks = new class() implements \IteratorAggregate { public int $called = 0; + /** + * @return \Iterator> + */ public function getIterator(): \Iterator { while (true) { // @phpstan-ignore-line diff --git a/tests/WaterfallTest.php b/tests/WaterfallTest.php index 2b274b2..be174a9 100644 --- a/tests/WaterfallTest.php +++ b/tests/WaterfallTest.php @@ -12,6 +12,9 @@ class WaterfallTest extends TestCase { public function testWaterfallWithoutTasks(): void { + /** + * @var array> $tasks + */ $tasks = array(); $promise = React\Async\waterfall($tasks); @@ -165,6 +168,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis $tasks = new class() implements \IteratorAggregate { public int $called = 0; + /** + * @return \Iterator> + */ public function getIterator(): \Iterator { while (true) { // @phpstan-ignore-line diff --git a/tests/types/async.php b/tests/types/async.php new file mode 100644 index 0000000..b5ba8fe --- /dev/null +++ b/tests/types/async.php @@ -0,0 +1,17 @@ +', async(static fn (): bool => true)()); +assertType('React\Promise\PromiseInterface', async(static fn (): PromiseInterface => resolve(true))()); +assertType('React\Promise\PromiseInterface', async(static fn (): bool => await(resolve(true)))()); + +assertType('React\Promise\PromiseInterface', async(static fn (int $a): int => $a)(42)); +assertType('React\Promise\PromiseInterface', async(static fn (int $a, int $b): int => $a + $b)(10, 32)); +assertType('React\Promise\PromiseInterface', async(static fn (int $a, int $b, int $c): int => $a + $b + $c)(10, 22, 10)); +assertType('React\Promise\PromiseInterface', async(static fn (int $a, int $b, int $c, int $d): int => $a + $b + $c + $d)(10, 22, 5, 5)); +assertType('React\Promise\PromiseInterface', async(static fn (int $a, int $b, int $c, int $d, int $e): int => $a + $b + $c + $d + $e)(10, 12, 10, 5, 5)); diff --git a/tests/types/await.php b/tests/types/await.php new file mode 100644 index 0000000..07d51b6 --- /dev/null +++ b/tests/types/await.php @@ -0,0 +1,23 @@ + true)())); +assertType('bool', await(async(static fn (): PromiseInterface => resolve(true))())); +assertType('bool', await(async(static fn (): 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); diff --git a/tests/types/coroutine.php b/tests/types/coroutine.php new file mode 100644 index 0000000..4c0f84c --- /dev/null +++ b/tests/types/coroutine.php @@ -0,0 +1,60 @@ +', coroutine(static function () { + return true; +})); + +assertType('React\Promise\PromiseInterface', coroutine(static function () { + return resolve(true); +})); + +// assertType('React\Promise\PromiseInterface', coroutine(static function () { +// return (yield resolve(true)); +// })); + +assertType('React\Promise\PromiseInterface', coroutine(static function () { +// $bool = yield resolve(true); +// assertType('bool', $bool); + + return time(); +})); + +// assertType('React\Promise\PromiseInterface', coroutine(static function () { +// $bool = yield resolve(true); +// assertType('bool', $bool); + +// return $bool; +// })); + +assertType('React\Promise\PromiseInterface', coroutine(static function () { + yield resolve(time()); + + return true; +})); + +assertType('React\Promise\PromiseInterface', coroutine(static function () { + for ($i = 0; $i <= 10; $i++) { + yield resolve($i); + } + + return true; +})); + +assertType('React\Promise\PromiseInterface', coroutine(static fn(int $a): int => $a, 42)); +assertType('React\Promise\PromiseInterface', coroutine(static fn(int $a, int $b): int => $a + $b, 10, 32)); +assertType('React\Promise\PromiseInterface', coroutine(static fn(int $a, int $b, int $c): int => $a + $b + $c, 10, 22, 10)); +assertType('React\Promise\PromiseInterface', coroutine(static fn(int $a, int $b, int $c, int $d): int => $a + $b + $c + $d, 10, 22, 5, 5)); +assertType('React\Promise\PromiseInterface', coroutine(static fn(int $a, int $b, int $c, int $d, int $e): int => $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)); +// }))); diff --git a/tests/types/parallel.php b/tests/types/parallel.php new file mode 100644 index 0000000..dacd024 --- /dev/null +++ b/tests/types/parallel.php @@ -0,0 +1,33 @@ +', parallel([])); + +assertType('React\Promise\PromiseInterface>', parallel([ + static fn (): PromiseInterface => resolve(true), + static fn (): PromiseInterface => resolve(time()), + static fn (): PromiseInterface => resolve(microtime(true)), +])); + +assertType('React\Promise\PromiseInterface>', parallel([ + static fn (): bool => true, + static fn (): int => time(), + static fn (): float => microtime(true), +])); + +assertType('array', await(parallel([ + static fn (): PromiseInterface => resolve(true), + static fn (): PromiseInterface => resolve(time()), + static fn (): PromiseInterface => resolve(microtime(true)), +]))); + +assertType('array', await(parallel([ + static fn (): bool => true, + static fn (): int => time(), + static fn (): float => microtime(true), +]))); diff --git a/tests/types/series.php b/tests/types/series.php new file mode 100644 index 0000000..9a233e3 --- /dev/null +++ b/tests/types/series.php @@ -0,0 +1,33 @@ +', series([])); + +assertType('React\Promise\PromiseInterface>', series([ + static fn (): PromiseInterface => resolve(true), + static fn (): PromiseInterface => resolve(time()), + static fn (): PromiseInterface => resolve(microtime(true)), +])); + +assertType('React\Promise\PromiseInterface>', series([ + static fn (): bool => true, + static fn (): int => time(), + static fn (): float => microtime(true), +])); + +assertType('array', await(series([ + static fn (): PromiseInterface => resolve(true), + static fn (): PromiseInterface => resolve(time()), + static fn (): PromiseInterface => resolve(microtime(true)), +]))); + +assertType('array', await(series([ + static fn (): bool => true, + static fn (): int => time(), + static fn (): float => microtime(true), +]))); diff --git a/tests/types/waterfall.php b/tests/types/waterfall.php new file mode 100644 index 0000000..1470785 --- /dev/null +++ b/tests/types/waterfall.php @@ -0,0 +1,42 @@ +', waterfall([])); + +assertType('React\Promise\PromiseInterface', waterfall([ + static fn (): PromiseInterface => resolve(microtime(true)), +])); + +assertType('React\Promise\PromiseInterface', waterfall([ + static fn (): float => microtime(true), +])); + +// Desired, but currently unsupported with the current set of templates +//assertType('React\Promise\PromiseInterface', waterfall([ +// static fn (): PromiseInterface => resolve(true), +// static fn (bool $bool): PromiseInterface => resolve(time()), +// static fn (int $int): PromiseInterface => resolve(microtime(true)), +//])); + +assertType('float', await(waterfall([ + static fn (): PromiseInterface => resolve(microtime(true)), +]))); + +// Desired, but currently unsupported with the current set of templates +//assertType('float', await(waterfall([ +// static fn (): PromiseInterface => resolve(true), +// static fn (bool $bool): PromiseInterface => resolve(time()), +// static fn (int $int): PromiseInterface => resolve(microtime(true)), +//]))); + +// assertType('React\Promise\PromiseInterface', waterfall(new EmptyIterator())); + +$iterator = new ArrayIterator([ + static fn (): PromiseInterface => resolve(true), +]); +assertType('React\Promise\PromiseInterface', waterfall($iterator));