Skip to content

Commit c41af0c

Browse files
authored
Merge pull request #82 from clue-labs/template-types-v3
[3.x] Add template annotations
2 parents 037cbf2 + d1c9606 commit c41af0c

File tree

12 files changed

+239
-27
lines changed

12 files changed

+239
-27
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ jobs:
4343
- 7.4
4444
- 7.3
4545
- 7.2
46-
- 7.1
4746
steps:
4847
- uses: actions/checkout@v3
4948
- uses: shivammathur/setup-php@v2

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ Async\await(…);
5757

5858
### await()
5959

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

6363
```php
@@ -94,7 +94,7 @@ try {
9494

9595
### coroutine()
9696

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

100100
```php
@@ -277,7 +277,7 @@ trigger at the earliest possible time in the future.
277277

278278
### parallel()
279279

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

283283
```php
@@ -319,7 +319,7 @@ React\Async\parallel([
319319

320320
### series()
321321

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

325325
```php
@@ -361,7 +361,7 @@ React\Async\series([
361361

362362
### waterfall()
363363

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

367367
```php

src/functions.php

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@
4444
* }
4545
* ```
4646
*
47-
* @param PromiseInterface $promise
48-
* @return mixed returns whatever the promise resolves to
47+
* @template T
48+
* @param PromiseInterface<T> $promise
49+
* @return T returns whatever the promise resolves to
4950
* @throws \Exception when the promise is rejected with an `Exception`
5051
* @throws \Throwable when the promise is rejected with a `Throwable`
5152
* @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) {
9394
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
9495
if (!$exception instanceof \Throwable) {
9596
$exception = new \UnexpectedValueException(
96-
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
97+
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception)) // @phpstan-ignore-line
9798
);
9899
}
99100

100101
throw $exception;
101102
}
102103

104+
/** @var T $resolved */
103105
return $resolved;
104106
}
105107

@@ -296,9 +298,16 @@ function delay(float $seconds): void
296298
* });
297299
* ```
298300
*
299-
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
301+
* @template T
302+
* @template TYield
303+
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
304+
* @template A2
305+
* @template A3
306+
* @template A4
307+
* @template A5
308+
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
300309
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
301-
* @return PromiseInterface<mixed>
310+
* @return PromiseInterface<T>
302311
* @since 3.0.0
303312
*/
304313
function coroutine(callable $function, ...$args): PromiseInterface
@@ -315,7 +324,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
315324

316325
$promise = null;
317326
$deferred = new Deferred(function () use (&$promise) {
318-
/** @var ?PromiseInterface $promise */
327+
/** @var ?PromiseInterface<T> $promise */
319328
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
320329
$promise->cancel();
321330
}
@@ -336,7 +345,6 @@ function coroutine(callable $function, ...$args): PromiseInterface
336345
return;
337346
}
338347

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

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

366375
/**
367-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
368-
* @return PromiseInterface<array<mixed>>
376+
* @template T
377+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
378+
* @return PromiseInterface<array<T>>
369379
*/
370380
function parallel(iterable $tasks): PromiseInterface
371381
{
372-
/** @var array<int,PromiseInterface> $pending */
382+
/** @var array<int,PromiseInterface<T>> $pending */
373383
$pending = [];
374384
$deferred = new Deferred(function () use (&$pending) {
375385
foreach ($pending as $promise) {
@@ -424,14 +434,15 @@ function parallel(iterable $tasks): PromiseInterface
424434
}
425435

426436
/**
427-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
428-
* @return PromiseInterface<array<mixed>>
437+
* @template T
438+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
439+
* @return PromiseInterface<array<T>>
429440
*/
430441
function series(iterable $tasks): PromiseInterface
431442
{
432443
$pending = null;
433444
$deferred = new Deferred(function () use (&$pending) {
434-
/** @var ?PromiseInterface $pending */
445+
/** @var ?PromiseInterface<T> $pending */
435446
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
436447
$pending->cancel();
437448
}
@@ -478,14 +489,15 @@ function series(iterable $tasks): PromiseInterface
478489
}
479490

480491
/**
481-
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
482-
* @return PromiseInterface<mixed>
492+
* @template T
493+
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
494+
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
483495
*/
484496
function waterfall(iterable $tasks): PromiseInterface
485497
{
486498
$pending = null;
487499
$deferred = new Deferred(function () use (&$pending) {
488-
/** @var ?PromiseInterface $pending */
500+
/** @var ?PromiseInterface<T> $pending */
489501
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
490502
$pending->cancel();
491503
}

tests/CoroutineTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately
2222
{
2323
$promise = coroutine(function () {
2424
if (false) { // @phpstan-ignore-line
25-
yield;
25+
yield resolve(null);
2626
}
2727
return 42;
2828
});
@@ -53,7 +53,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately()
5353
{
5454
$promise = coroutine(function () {
5555
if (false) { // @phpstan-ignore-line
56-
yield;
56+
yield resolve(null);
5757
}
5858
throw new \RuntimeException('Foo');
5959
});
@@ -99,7 +99,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi
9999

100100
public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void
101101
{
102-
$promise = coroutine(function () {
102+
$promise = coroutine(function () { // @phpstan-ignore-line
103103
yield 42;
104104
});
105105

@@ -169,7 +169,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet
169169

170170
$promise = coroutine(function () {
171171
if (false) { // @phpstan-ignore-line
172-
yield;
172+
yield resolve(null);
173173
}
174174
return 42;
175175
});
@@ -249,7 +249,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYie
249249

250250
gc_collect_cycles();
251251

252-
$promise = coroutine(function () {
252+
$promise = coroutine(function () { // @phpstan-ignore-line
253253
yield 42;
254254
});
255255

tests/ParallelTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class ParallelTest extends TestCase
1212
{
1313
public function testParallelWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

1720
$promise = React\Async\parallel($tasks);

tests/SeriesTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class SeriesTest extends TestCase
1212
{
1313
public function testSeriesWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

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

158+
/**
159+
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
160+
*/
155161
public function getIterator(): \Iterator
156162
{
157163
while (true) { // @phpstan-ignore-line

tests/WaterfallTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ class WaterfallTest extends TestCase
1212
{
1313
public function testWaterfallWithoutTasks(): void
1414
{
15+
/**
16+
* @var array<callable(): React\Promise\PromiseInterface<mixed>> $tasks
17+
*/
1518
$tasks = array();
1619

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

172+
/**
173+
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
174+
*/
169175
public function getIterator(): \Iterator
170176
{
171177
while (true) { // @phpstan-ignore-line

tests/types/await.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
use function PHPStan\Testing\assertType;
4+
use function React\Async\await;
5+
use function React\Promise\resolve;
6+
7+
assertType('bool', await(resolve(true)));
8+
9+
final class AwaitExampleUser
10+
{
11+
public string $name;
12+
13+
public function __construct(string $name) {
14+
$this->name = $name;
15+
}
16+
}
17+
18+
assertType('string', await(resolve(new AwaitExampleUser('WyriHaximus')))->name);

tests/types/coroutine.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
use function PHPStan\Testing\assertType;
4+
use function React\Async\await;
5+
use function React\Async\coroutine;
6+
use function React\Promise\resolve;
7+
8+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
9+
return true;
10+
}));
11+
12+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
13+
return resolve(true);
14+
}));
15+
16+
// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
17+
// return (yield resolve(true));
18+
// }));
19+
20+
assertType('React\Promise\PromiseInterface<int>', coroutine(static function () {
21+
// $bool = yield resolve(true);
22+
// assertType('bool', $bool);
23+
24+
return time();
25+
}));
26+
27+
// assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
28+
// $bool = yield resolve(true);
29+
// assertType('bool', $bool);
30+
31+
// return $bool;
32+
// }));
33+
34+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
35+
yield resolve(time());
36+
37+
return true;
38+
}));
39+
40+
assertType('React\Promise\PromiseInterface<bool>', coroutine(static function () {
41+
for ($i = 0; $i <= 10; $i++) {
42+
yield resolve($i);
43+
}
44+
45+
return true;
46+
}));
47+
48+
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a): int { return $a; }, 42));
49+
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b): int { return $a + $b; }, 10, 32));
50+
assertType('React\Promise\PromiseInterface<int>', coroutine(static function (int $a, int $b, int $c): int { return $a + $b + $c; }, 10, 22, 10));
51+
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));
52+
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));
53+
54+
assertType('bool', await(coroutine(static function () {
55+
return true;
56+
})));
57+
58+
// assertType('bool', await(coroutine(static function () {
59+
// return (yield resolve(true));
60+
// })));

tests/types/parallel.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
use React\Promise\PromiseInterface;
4+
use function PHPStan\Testing\assertType;
5+
use function React\Async\await;
6+
use function React\Async\parallel;
7+
use function React\Promise\resolve;
8+
9+
assertType('React\Promise\PromiseInterface<array>', parallel([]));
10+
11+
assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
12+
static function (): PromiseInterface { return resolve(true); },
13+
static function (): PromiseInterface { return resolve(time()); },
14+
static function (): PromiseInterface { return resolve(microtime(true)); },
15+
]));
16+
17+
assertType('React\Promise\PromiseInterface<array<bool|float|int>>', parallel([
18+
static function (): bool { return true; },
19+
static function (): int { return time(); },
20+
static function (): float { return microtime(true); },
21+
]));
22+
23+
assertType('array<bool|float|int>', await(parallel([
24+
static function (): PromiseInterface { return resolve(true); },
25+
static function (): PromiseInterface { return resolve(time()); },
26+
static function (): PromiseInterface { return resolve(microtime(true)); },
27+
])));
28+
29+
assertType('array<bool|float|int>', await(parallel([
30+
static function (): bool { return true; },
31+
static function (): int { return time(); },
32+
static function (): float { return microtime(true); },
33+
])));

0 commit comments

Comments
 (0)