Skip to content

Commit b14580d

Browse files
WyriHaximusclue
authored andcommitted
[3.x] Add template annotations
Adds template annotations turning the `PromiseInterface` into a generic. Variables `$p1` and `$p2` in the following code example both are `PromiseInterface<int|string>`. ```php $f = function (): int|string { return time() % 2 ? 'string' : time(); }; /** * @return PromiseInterface<int|string> */ $fp = function (): PromiseInterface { return resolve(time() % 2 ? 'string' : time()); }; $p1 = resolve($f()); $p2 = $fp(); ``` When calling `then` on `$p1` or `$p2`, PHPStan understand that function `$f1` is type hinting its parameter fine, but `$f2` will throw during runtime: ```php $p2->then(static function (int|string $a) {}); $p2->then(static function (bool $a) {}); ``` Builds on top of #246 and #188 and is a requirement for reactphp/async#40
1 parent d87b562 commit b14580d

26 files changed

+355
-51
lines changed

.github/workflows/ci.yml

-1
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

src/Deferred.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
namespace React\Promise;
44

5+
/**
6+
* @template T
7+
*/
58
final class Deferred
69
{
7-
/** @var Promise */
10+
/**
11+
* @var PromiseInterface<T>
12+
*/
813
private $promise;
914

1015
/** @var callable */
@@ -21,13 +26,16 @@ public function __construct(callable $canceller = null)
2126
}, $canceller);
2227
}
2328

29+
/**
30+
* @return PromiseInterface<T>
31+
*/
2432
public function promise(): PromiseInterface
2533
{
2634
return $this->promise;
2735
}
2836

2937
/**
30-
* @param mixed $value
38+
* @param T $value
3139
*/
3240
public function resolve($value): void
3341
{

src/Internal/FulfilledPromise.php

+15-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
/**
99
* @internal
10+
*
11+
* @template T
12+
* @template-implements PromiseInterface<T>
1013
*/
1114
final class FulfilledPromise implements PromiseInterface
1215
{
13-
/** @var mixed */
16+
/** @var T */
1417
private $value;
1518

1619
/**
17-
* @param mixed $value
20+
* @param T $value
1821
* @throws \InvalidArgumentException
1922
*/
2023
public function __construct($value = null)
@@ -26,14 +29,23 @@ public function __construct($value = null)
2629
$this->value = $value;
2730
}
2831

32+
/**
33+
* @template TFulfilled
34+
* @param ?(callable((T is void ? null : T)): (PromiseInterface<TFulfilled>|TFulfilled)) $onFulfilled
35+
* @return PromiseInterface<($onFulfilled is null ? T : TFulfilled)>
36+
*/
2937
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
3038
{
3139
if (null === $onFulfilled) {
3240
return $this;
3341
}
3442

3543
try {
36-
return resolve($onFulfilled($this->value));
44+
/**
45+
* @var PromiseInterface<T>|T $result
46+
*/
47+
$result = $onFulfilled($this->value);
48+
return resolve($result);
3749
} catch (\Throwable $exception) {
3850
return new RejectedPromise($exception);
3951
}

src/Internal/RejectedPromise.php

+17
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
/**
1010
* @internal
11+
*
12+
* @template-implements PromiseInterface<never>
1113
*/
1214
final class RejectedPromise implements PromiseInterface
1315
{
@@ -37,6 +39,12 @@ public function __destruct()
3739
\error_log($message);
3840
}
3941

42+
/**
43+
* @template TRejected
44+
* @param ?callable $onFulfilled
45+
* @param ?(callable(\Throwable): (PromiseInterface<TRejected>|TRejected)) $onRejected
46+
* @return PromiseInterface<($onRejected is null ? never : TRejected)>
47+
*/
4048
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
4149
{
4250
if (null === $onRejected) {
@@ -52,12 +60,21 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
5260
}
5361
}
5462

63+
/**
64+
* @template TThrowable of \Throwable
65+
* @template TRejected
66+
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
67+
* @return PromiseInterface<TRejected>
68+
*/
5569
public function catch(callable $onRejected): PromiseInterface
5670
{
5771
if (!_checkTypehint($onRejected, $this->reason)) {
5872
return $this;
5973
}
6074

75+
/**
76+
* @var callable(\Throwable):(PromiseInterface<TRejected>|TRejected) $onRejected
77+
*/
6178
return $this->then(null, $onRejected);
6279
}
6380

src/Promise.php

+22-1
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
use React\Promise\Internal\RejectedPromise;
66

7+
/**
8+
* @template T
9+
* @template-implements PromiseInterface<T>
10+
*/
711
final class Promise implements PromiseInterface
812
{
913
/** @var ?callable */
1014
private $canceller;
1115

12-
/** @var ?PromiseInterface */
16+
/** @var ?PromiseInterface<T> */
1317
private $result;
1418

1519
/** @var callable[] */
@@ -66,13 +70,22 @@ static function () use (&$parent) {
6670
);
6771
}
6872

73+
/**
74+
* @template TThrowable of \Throwable
75+
* @template TRejected
76+
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
77+
* @return PromiseInterface<T|TRejected>
78+
*/
6979
public function catch(callable $onRejected): PromiseInterface
7080
{
7181
return $this->then(null, static function ($reason) use ($onRejected) {
7282
if (!_checkTypehint($onRejected, $reason)) {
7383
return new RejectedPromise($reason);
7484
}
7585

86+
/**
87+
* @var callable(\Throwable):(PromiseInterface<TRejected>|TRejected) $onRejected
88+
*/
7689
return $onRejected($reason);
7790
});
7891
}
@@ -175,6 +188,9 @@ private function reject(\Throwable $reason): void
175188
$this->settle(reject($reason));
176189
}
177190

191+
/**
192+
* @param PromiseInterface<T> $result
193+
*/
178194
private function settle(PromiseInterface $result): void
179195
{
180196
$result = $this->unwrap($result);
@@ -207,9 +223,14 @@ private function settle(PromiseInterface $result): void
207223
}
208224
}
209225

226+
/**
227+
* @param PromiseInterface<T> $promise
228+
* @return PromiseInterface<T>
229+
*/
210230
private function unwrap(PromiseInterface $promise): PromiseInterface
211231
{
212232
while ($promise instanceof self && null !== $promise->result) {
233+
/** @var PromiseInterface<T> $promise */
213234
$promise = $promise->result;
214235
}
215236

src/PromiseInterface.php

+20-11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace React\Promise;
44

5+
/**
6+
* @template-covariant T
7+
*/
58
interface PromiseInterface
69
{
710
/**
@@ -28,9 +31,11 @@ interface PromiseInterface
2831
* 2. `$onFulfilled` and `$onRejected` will never be called more
2932
* than once.
3033
*
31-
* @param callable|null $onFulfilled
32-
* @param callable|null $onRejected
33-
* @return PromiseInterface
34+
* @template TFulfilled
35+
* @template TRejected
36+
* @param ?(callable((T is void ? null : T)): (PromiseInterface<TFulfilled>|TFulfilled)) $onFulfilled
37+
* @param ?(callable(\Throwable): (PromiseInterface<TRejected>|TRejected)) $onRejected
38+
* @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))>
3439
*/
3540
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface;
3641

@@ -44,8 +49,10 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null
4449
* Additionally, you can type hint the `$reason` argument of `$onRejected` to catch
4550
* only specific errors.
4651
*
47-
* @param callable $onRejected
48-
* @return PromiseInterface
52+
* @template TThrowable of \Throwable
53+
* @template TRejected
54+
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
55+
* @return PromiseInterface<T|TRejected>
4956
*/
5057
public function catch(callable $onRejected): PromiseInterface;
5158

@@ -91,8 +98,8 @@ public function catch(callable $onRejected): PromiseInterface;
9198
* ->finally('cleanup');
9299
* ```
93100
*
94-
* @param callable $onFulfilledOrRejected
95-
* @return PromiseInterface
101+
* @param callable(): (void|PromiseInterface<void>) $onFulfilledOrRejected
102+
* @return PromiseInterface<T>
96103
*/
97104
public function finally(callable $onFulfilledOrRejected): PromiseInterface;
98105

@@ -117,8 +124,10 @@ public function cancel(): void;
117124
* $promise->catch($onRejected);
118125
* ```
119126
*
120-
* @param callable $onRejected
121-
* @return PromiseInterface
127+
* @template TThrowable of \Throwable
128+
* @template TRejected
129+
* @param callable(TThrowable): (PromiseInterface<TRejected>|TRejected) $onRejected
130+
* @return PromiseInterface<T|TRejected>
122131
* @deprecated 3.0.0 Use catch() instead
123132
* @see self::catch()
124133
*/
@@ -134,8 +143,8 @@ public function otherwise(callable $onRejected): PromiseInterface;
134143
* $promise->finally($onFulfilledOrRejected);
135144
* ```
136145
*
137-
* @param callable $onFulfilledOrRejected
138-
* @return PromiseInterface
146+
* @param callable(): (void|PromiseInterface<void>) $onFulfilledOrRejected
147+
* @return PromiseInterface<T>
139148
* @deprecated 3.0.0 Use finally() instead
140149
* @see self::finally()
141150
*/

src/functions.php

+15-11
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
*
1818
* If `$promiseOrValue` is a promise, it will be returned as is.
1919
*
20-
* @param mixed $promiseOrValue
21-
* @return PromiseInterface
20+
* @template T
21+
* @param PromiseInterface<T>|T $promiseOrValue
22+
* @return PromiseInterface<T>
2223
*/
2324
function resolve($promiseOrValue): PromiseInterface
2425
{
@@ -31,6 +32,7 @@ function resolve($promiseOrValue): PromiseInterface
3132

3233
if (\method_exists($promiseOrValue, 'cancel')) {
3334
$canceller = [$promiseOrValue, 'cancel'];
35+
assert(\is_callable($canceller));
3436
}
3537

3638
return new Promise(function ($resolve, $reject) use ($promiseOrValue): void {
@@ -54,8 +56,7 @@ function resolve($promiseOrValue): PromiseInterface
5456
* throwing an exception. For example, it allows you to propagate a rejection with
5557
* the value of another promise.
5658
*
57-
* @param \Throwable $reason
58-
* @return PromiseInterface
59+
* @return PromiseInterface<never>
5960
*/
6061
function reject(\Throwable $reason): PromiseInterface
6162
{
@@ -68,8 +69,9 @@ function reject(\Throwable $reason): PromiseInterface
6869
* will be an array containing the resolution values of each of the items in
6970
* `$promisesOrValues`.
7071
*
71-
* @param iterable<mixed> $promisesOrValues
72-
* @return PromiseInterface
72+
* @template T
73+
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
74+
* @return PromiseInterface<array<T>>
7375
*/
7476
function all(iterable $promisesOrValues): PromiseInterface
7577
{
@@ -119,14 +121,15 @@ function (\Throwable $reason) use (&$continue, $reject): void {
119121
* The returned promise will become **infinitely pending** if `$promisesOrValues`
120122
* contains 0 items.
121123
*
122-
* @param iterable<mixed> $promisesOrValues
123-
* @return PromiseInterface
124+
* @template T
125+
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
126+
* @return PromiseInterface<T>
124127
*/
125128
function race(iterable $promisesOrValues): PromiseInterface
126129
{
127130
$cancellationQueue = new Internal\CancellationQueue();
128131

129-
return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
132+
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
130133
$continue = true;
131134

132135
foreach ($promisesOrValues as $promiseOrValue) {
@@ -154,8 +157,9 @@ function race(iterable $promisesOrValues): PromiseInterface
154157
* The returned promise will also reject with a `React\Promise\Exception\LengthException`
155158
* if `$promisesOrValues` contains 0 items.
156159
*
157-
* @param iterable<mixed> $promisesOrValues
158-
* @return PromiseInterface
160+
* @template T
161+
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
162+
* @return PromiseInterface<T>
159163
*/
160164
function any(iterable $promisesOrValues): PromiseInterface
161165
{

tests/DeferredTest.php

+7-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44

55
use React\Promise\PromiseAdapter\CallbackPromiseAdapter;
66

7+
/**
8+
* @template T
9+
*/
710
class DeferredTest extends TestCase
811
{
912
use PromiseTest\FullTestTrait;
1013

14+
/**
15+
* @return CallbackPromiseAdapter<T>
16+
*/
1117
public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter
1218
{
1319
$d = new Deferred($canceller);
@@ -54,7 +60,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
5460
gc_collect_cycles();
5561
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
5662

57-
/** @var Deferred $deferred */
63+
/** @var Deferred<never> $deferred */
5864
$deferred = new Deferred(function () use (&$deferred) {
5965
assert($deferred instanceof Deferred);
6066
});

tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use function React\Promise\reject;
1212

1313
require __DIR__ . '/../vendor/autoload.php';
1414

15-
reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void {
15+
reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line
1616
echo 'This will never be shown because the types do not match' . PHP_EOL;
1717
});
1818

tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use function React\Promise\reject;
1212

1313
require __DIR__ . '/../vendor/autoload.php';
1414

15-
reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void {
15+
reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line
1616
echo 'This will never be shown because the types do not match' . PHP_EOL;
1717
});
1818

tests/Internal/CancellationQueueTest.php

+3
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ public function rethrowsExceptionsThrownFromCancel(): void
9696
$cancellationQueue();
9797
}
9898

99+
/**
100+
* @return Deferred<never>
101+
*/
99102
private function getCancellableDeferred(): Deferred
100103
{
101104
return new Deferred($this->expectCallableOnce());

0 commit comments

Comments
 (0)