Skip to content

Commit 8cc37cc

Browse files
authored
Merge pull request #40 from WyriHaximus-labs/4.x-add-template-annotations
[4.x] Add template annotations
2 parents 307684c + 643316a commit 8cc37cc

14 files changed

+288
-34
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Async\await(…);
5858

5959
### async()
6060

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

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

227227
### await()
228228

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

232232
```php
@@ -278,7 +278,7 @@ try {
278278

279279
### coroutine()
280280

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

284284
```php
@@ -498,7 +498,7 @@ Loop::addTimer(2.0, function () use ($promise): void {
498498

499499
### parallel()
500500

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

504504
```php
@@ -540,7 +540,7 @@ React\Async\parallel([
540540

541541
### series()
542542

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

546546
```php
@@ -582,7 +582,7 @@ React\Async\series([
582582

583583
### waterfall()
584584

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

588588
```php

src/FiberMap.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66

77
/**
88
* @internal
9+
*
10+
* @template T
911
*/
1012
final class FiberMap
1113
{
1214
/** @var array<int,bool> */
1315
private static array $status = [];
1416

15-
/** @var array<int,PromiseInterface> */
17+
/** @var array<int,PromiseInterface<T>> */
1618
private static array $map = [];
1719

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

30-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
32+
/**
33+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
34+
* @param PromiseInterface<T> $promise
35+
*/
3136
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
3237
{
3338
self::$map[\spl_object_id($fiber)] = $promise;
3439
}
3540

36-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
41+
/**
42+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
43+
* @param PromiseInterface<T> $promise
44+
*/
3745
public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void
3846
{
3947
unset(self::$map[\spl_object_id($fiber)]);
4048
}
4149

42-
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
50+
/**
51+
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
52+
* @return ?PromiseInterface<T>
53+
*/
4354
public static function getPromise(\Fiber $fiber): ?PromiseInterface
4455
{
4556
return self::$map[\spl_object_id($fiber)] ?? null;

src/functions.php

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,14 @@
176176
* await($promise);
177177
* ```
178178
*
179-
* @param callable $function
180-
* @return callable(mixed ...): PromiseInterface<mixed>
179+
* @template T
180+
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
181+
* @template A2
182+
* @template A3
183+
* @template A4
184+
* @template A5
185+
* @param callable(A1,A2,A3,A4,A5): (PromiseInterface<T>|T) $function
186+
* @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface<T>
181187
* @since 4.0.0
182188
* @see coroutine()
183189
*/
@@ -268,8 +274,9 @@ function async(callable $function): callable
268274
* }
269275
* ```
270276
*
271-
* @param PromiseInterface $promise
272-
* @return mixed returns whatever the promise resolves to
277+
* @template T
278+
* @param PromiseInterface<T> $promise
279+
* @return T returns whatever the promise resolves to
273280
* @throws \Exception when the promise is rejected with an `Exception`
274281
* @throws \Throwable when the promise is rejected with a `Throwable`
275282
* @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
279286
$fiber = null;
280287
$resolved = false;
281288
$rejected = false;
289+
290+
/** @var T $resolvedValue */
282291
$resolvedValue = null;
283292
$rejectedThrowable = null;
284293
$lowLevelFiber = \Fiber::getCurrent();
@@ -292,6 +301,7 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe
292301
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
293302
if ($fiber === null) {
294303
$resolved = true;
304+
/** @var T $resolvedValue */
295305
$resolvedValue = $value;
296306
return;
297307
}
@@ -305,7 +315,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
305315

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

311321
// avoid garbage references by replacing all closures in call stack.
@@ -592,9 +602,16 @@ function delay(float $seconds): void
592602
* });
593603
* ```
594604
*
595-
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
605+
* @template T
606+
* @template TYield
607+
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
608+
* @template A2
609+
* @template A3
610+
* @template A4
611+
* @template A5
612+
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
596613
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
597-
* @return PromiseInterface<mixed>
614+
* @return PromiseInterface<T>
598615
* @since 3.0.0
599616
*/
600617
function coroutine(callable $function, mixed ...$args): PromiseInterface
@@ -611,7 +628,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
611628

612629
$promise = null;
613630
$deferred = new Deferred(function () use (&$promise) {
614-
/** @var ?PromiseInterface $promise */
631+
/** @var ?PromiseInterface<T> $promise */
615632
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
616633
$promise->cancel();
617634
}
@@ -632,7 +649,6 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
632649
return;
633650
}
634651

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

661+
/** @var PromiseInterface<TYield> $promise */
645662
assert($next instanceof \Closure);
646663
$promise->then(function ($value) use ($generator, $next) {
647664
$generator->send($value);
@@ -660,12 +677,13 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
660677
}
661678

662679
/**
663-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
664-
* @return PromiseInterface<array<mixed>>
680+
* @template T
681+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
682+
* @return PromiseInterface<array<T>>
665683
*/
666684
function parallel(iterable $tasks): PromiseInterface
667685
{
668-
/** @var array<int,PromiseInterface> $pending */
686+
/** @var array<int,PromiseInterface<T>> $pending */
669687
$pending = [];
670688
$deferred = new Deferred(function () use (&$pending) {
671689
foreach ($pending as $promise) {
@@ -720,14 +738,15 @@ function parallel(iterable $tasks): PromiseInterface
720738
}
721739

722740
/**
723-
* @param iterable<callable():PromiseInterface<mixed>> $tasks
724-
* @return PromiseInterface<array<mixed>>
741+
* @template T
742+
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
743+
* @return PromiseInterface<array<T>>
725744
*/
726745
function series(iterable $tasks): PromiseInterface
727746
{
728747
$pending = null;
729748
$deferred = new Deferred(function () use (&$pending) {
730-
/** @var ?PromiseInterface $pending */
749+
/** @var ?PromiseInterface<T> $pending */
731750
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
732751
$pending->cancel();
733752
}
@@ -774,14 +793,15 @@ function series(iterable $tasks): PromiseInterface
774793
}
775794

776795
/**
777-
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
778-
* @return PromiseInterface<mixed>
796+
* @template T
797+
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
798+
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
779799
*/
780800
function waterfall(iterable $tasks): PromiseInterface
781801
{
782802
$pending = null;
783803
$deferred = new Deferred(function () use (&$pending) {
784-
/** @var ?PromiseInterface $pending */
804+
/** @var ?PromiseInterface<T> $pending */
785805
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
786806
$pending->cancel();
787807
}

tests/AwaitTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@ public function testRejectedPromisesShouldBeDetached(callable $await): void
413413
})());
414414
}
415415

416-
/** @return iterable<string,list<callable(PromiseInterface): mixed>> */
416+
/** @return iterable<string,list<callable(PromiseInterface<mixed>): mixed>> */
417417
public function provideAwaiters(): iterable
418418
{
419419
yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];

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);
@@ -151,6 +154,9 @@ public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRe
151154
$tasks = new class() implements \IteratorAggregate {
152155
public int $called = 0;
153156

157+
/**
158+
* @return \Iterator<callable(): React\Promise\PromiseInterface<mixed>>
159+
*/
154160
public function getIterator(): \Iterator
155161
{
156162
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);
@@ -165,6 +168,9 @@ public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromis
165168
$tasks = new class() implements \IteratorAggregate {
166169
public int $called = 0;
167170

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

tests/types/async.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
use React\Promise\PromiseInterface;
4+
use function PHPStan\Testing\assertType;
5+
use function React\Async\async;
6+
use function React\Async\await;
7+
use function React\Promise\resolve;
8+
9+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => true)());
10+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): PromiseInterface => resolve(true))());
11+
assertType('React\Promise\PromiseInterface<bool>', async(static fn (): bool => await(resolve(true)))());
12+
13+
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a): int => $a)(42));
14+
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b): int => $a + $b)(10, 32));
15+
assertType('React\Promise\PromiseInterface<int>', async(static fn (int $a, int $b, int $c): int => $a + $b + $c)(10, 22, 10));
16+
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));
17+
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));

0 commit comments

Comments
 (0)