Skip to content

[3.x] Add template annotations #82

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

Merged
merged 1 commit into from
Nov 3, 2023
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -43,7 +43,6 @@ jobs:
- 7.4
- 7.3
- 7.2
- 7.1
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
44 changes: 28 additions & 16 deletions src/functions.php
Original file line number Diff line number Diff line change
@@ -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)
@@ -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;
}

@@ -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
@@ -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();
}
@@ -336,7 +345,6 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

/** @var mixed $promise */
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$next = null;
@@ -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);
@@ -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) {
@@ -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();
}
@@ -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();
}
10 changes: 5 additions & 5 deletions tests/CoroutineTest.php
Original file line number Diff line number Diff line change
@@ -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;
});

3 changes: 3 additions & 0 deletions tests/ParallelTest.php
Original file line number Diff line number Diff line change
@@ -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);
6 changes: 6 additions & 0 deletions tests/SeriesTest.php
Original file line number Diff line number Diff line change
@@ -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);
@@ -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
6 changes: 6 additions & 0 deletions tests/WaterfallTest.php
Original file line number Diff line number Diff line change
@@ -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);
@@ -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
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); },
])));
33 changes: 33 additions & 0 deletions tests/types/series.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\series;
use function React\Promise\resolve;

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

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

assertType('array<bool|float|int>', await(series([
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(series([
static function (): bool { return true; },
static function (): int { return time(); },
static function (): float { return microtime(true); },
])));
42 changes: 42 additions & 0 deletions tests/types/waterfall.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

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

assertType('React\Promise\PromiseInterface<null>', waterfall([]));

assertType('React\Promise\PromiseInterface<float>', waterfall([
static function (): PromiseInterface { return resolve(microtime(true)); },
]));

assertType('React\Promise\PromiseInterface<float>', waterfall([
static function (): float { return microtime(true); },
]));

// Desired, but currently unsupported with the current set of templates
//assertType('React\Promise\PromiseInterface<float>', waterfall([
// static function (): PromiseInterface { return resolve(true); },
// static function (bool $bool): PromiseInterface { return resolve(time()); },
// static function (int $int): PromiseInterface { return resolve(microtime(true)); },
//]));

assertType('float', await(waterfall([
static function (): PromiseInterface { return resolve(microtime(true)); },
])));

// Desired, but currently unsupported with the current set of templates
//assertType('float', await(waterfall([
// static function (): PromiseInterface { return resolve(true); },
// static function (bool $bool): PromiseInterface { return resolve(time()); },
// static function (int $int): PromiseInterface { return resolve(microtime(true)); },
//])));

// assertType('React\Promise\PromiseInterface<null>', waterfall(new EmptyIterator()));

$iterator = new ArrayIterator([
static function (): PromiseInterface { return resolve(true); },
]);
assertType('React\Promise\PromiseInterface<bool>', waterfall($iterator));