Skip to content

Commit e48bb63

Browse files
simPodWyriHaximus
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 d66fa66 commit e48bb63

21 files changed

+233
-94
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ jobs:
5252
coverage: none
5353
- run: composer install
5454
- run: vendor/bin/phpstan
55+
if: ${{ matrix.php >= 7.2 }}
56+
- run: vendor/bin/phpstan --configuration="phpstan.legacy.neon.dist"
57+
if: ${{ matrix.php < 7.2 }}

phpstan.legacy.neon.dist

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
ignoreErrors:
3+
- '#Template type T is declared as covariant, but occurs in contravariant position in parameter result of method React\\Promise\\Promise::settle\(\).#'
4+
- '#Template type T is declared as covariant, but occurs in contravariant position in parameter promise of method React\\Promise\\Promise::unwrap\(\).#'
5+
6+
includes:
7+
- phpstan.neon.dist

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ parameters:
44
paths:
55
- src/
66
- tests/
7+
- types/

src/Deferred.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
namespace React\Promise;
44

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

1015
/** @var callable */
@@ -21,6 +26,9 @@ 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;

src/Internal/FulfilledPromise.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77

88
/**
99
* @internal
10+
*
11+
* @template-implements PromiseInterface<T>
12+
* @template-covariant 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)

src/Internal/RejectedPromise.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
/**
1010
* @internal
11+
*
12+
* @template-implements PromiseInterface<T>
13+
* @template-covariant T
1114
*/
1215
final class RejectedPromise implements PromiseInterface
1316
{

src/Promise.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
use React\Promise\Internal\RejectedPromise;
66

7+
/**
8+
* @template-implements PromiseInterface<T>
9+
* @template-covariant 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[] */
@@ -77,11 +81,11 @@ public function catch(callable $onRejected): PromiseInterface
7781
public function finally(callable $onFulfilledOrRejected): PromiseInterface
7882
{
7983
return $this->then(static function ($value) use ($onFulfilledOrRejected) {
80-
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
84+
return resolve($onFulfilledOrRejected())->then(function ($_) use ($value) {
8185
return $value;
8286
});
8387
}, static function ($reason) use ($onFulfilledOrRejected) {
84-
return resolve($onFulfilledOrRejected())->then(function () use ($reason) {
88+
return resolve($onFulfilledOrRejected())->then(function ($_) use ($reason) {
8589
return new RejectedPromise($reason);
8690
});
8791
});
@@ -166,6 +170,9 @@ private function reject(\Throwable $reason): void
166170
$this->settle(reject($reason));
167171
}
168172

173+
/**
174+
* @param PromiseInterface<T>|PromiseInterface<null> $result
175+
*/
169176
private function settle(PromiseInterface $result): void
170177
{
171178
$result = $this->unwrap($result);
@@ -193,13 +200,17 @@ private function settle(PromiseInterface $result): void
193200
}
194201
}
195202

203+
/**
204+
* @param PromiseInterface<T>|PromiseInterface<null> $promise
205+
* @return PromiseInterface<T>
206+
*/
196207
private function unwrap(PromiseInterface $promise): PromiseInterface
197208
{
198209
while ($promise instanceof self && null !== $promise->result) {
199210
$promise = $promise->result;
200211
}
201212

202-
return $promise;
213+
return $promise; /** @phpstan-ignore-line */
203214
}
204215

205216
private function call(callable $cb): void

src/PromiseInterface.php

Lines changed: 12 additions & 9 deletions
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,9 @@ 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 as PromiseInterface<T>|T
35+
* @param (callable(T): TFulfilled)|null $onFulfilled
36+
* @return PromiseInterface<T>
3437
*/
3538
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface;
3639

@@ -44,8 +47,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null
4447
* Additionally, you can type hint the `$reason` argument of `$onRejected` to catch
4548
* only specific errors.
4649
*
47-
* @param callable $onRejected
48-
* @return PromiseInterface
50+
* @return PromiseInterface<T>
4951
*/
5052
public function catch(callable $onRejected): PromiseInterface;
5153

@@ -91,8 +93,9 @@ public function catch(callable $onRejected): PromiseInterface;
9193
* ->finally('cleanup');
9294
* ```
9395
*
94-
* @param callable $onFulfilledOrRejected
95-
* @return PromiseInterface
96+
* @template TReturn of PromiseInterface<T>|T
97+
* @param callable(T=): TReturn $onFulfilledOrRejected
98+
* @return PromiseInterface<T>
9699
*/
97100
public function finally(callable $onFulfilledOrRejected): PromiseInterface;
98101

@@ -118,7 +121,7 @@ public function cancel(): void;
118121
* ```
119122
*
120123
* @param callable $onRejected
121-
* @return PromiseInterface
124+
* @return PromiseInterface<T>
122125
* @deprecated 3.0.0 Use catch() instead
123126
* @see self::catch()
124127
*/
@@ -135,7 +138,7 @@ public function otherwise(callable $onRejected): PromiseInterface;
135138
* ```
136139
*
137140
* @param callable $onFulfilledOrRejected
138-
* @return PromiseInterface
141+
* @return PromiseInterface<T>
139142
* @deprecated 3.0.0 Use finally() instead
140143
* @see self::finally()
141144
*/

src/functions.php

Lines changed: 17 additions & 13 deletions
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
{
@@ -30,6 +31,7 @@ function resolve($promiseOrValue): PromiseInterface
3031
$canceller = null;
3132

3233
if (\method_exists($promiseOrValue, 'cancel')) {
34+
/** @var callable $canceller */
3335
$canceller = [$promiseOrValue, 'cancel'];
3436
}
3537

@@ -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<null>
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
{
@@ -87,7 +89,7 @@ function all(iterable $promisesOrValues): PromiseInterface
8789
++$toResolve;
8890

8991
resolve($promiseOrValue)->then(
90-
function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void {
92+
function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { /** @phpstan-ignore-line */
9193
$values[$i] = $value;
9294

9395
if (0 === --$toResolve && !$continue) {
@@ -119,8 +121,9 @@ 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
{
@@ -132,7 +135,7 @@ function race(iterable $promisesOrValues): PromiseInterface
132135
foreach ($promisesOrValues as $promiseOrValue) {
133136
$cancellationQueue->enqueue($promiseOrValue);
134137

135-
resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void {
138+
resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void { /** @phpstan-ignore-line */
136139
$continue = false;
137140
});
138141

@@ -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
{
@@ -171,7 +175,7 @@ function any(iterable $promisesOrValues): PromiseInterface
171175
++$toReject;
172176

173177
resolve($promiseOrValue)->then(
174-
function ($value) use ($resolve, &$continue): void {
178+
function ($value) use ($resolve, &$continue): void { /** @phpstan-ignore-line */
175179
$continue = false;
176180
$resolve($value);
177181
},

tests/FunctionAllTest.php

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ class FunctionAllTest extends TestCase
99
/** @test */
1010
public function shouldResolveEmptyInput(): void
1111
{
12-
$mock = $this->createCallableMock();
13-
$mock
14-
->expects(self::once())
12+
/** @var callable $mock */
13+
$mock = $_ = $this->createCallableMock();
14+
$_->expects(self::once())
1515
->method('__invoke')
1616
->with(self::identicalTo([]));
1717

@@ -22,9 +22,9 @@ public function shouldResolveEmptyInput(): void
2222
/** @test */
2323
public function shouldResolveValuesArray(): void
2424
{
25-
$mock = $this->createCallableMock();
26-
$mock
27-
->expects(self::once())
25+
/** @var callable $mock */
26+
$mock = $_ = $this->createCallableMock();
27+
$_->expects(self::once())
2828
->method('__invoke')
2929
->with(self::identicalTo([1, 2, 3]));
3030

@@ -35,9 +35,9 @@ public function shouldResolveValuesArray(): void
3535
/** @test */
3636
public function shouldResolvePromisesArray(): void
3737
{
38-
$mock = $this->createCallableMock();
39-
$mock
40-
->expects(self::once())
38+
/** @var callable $mock */
39+
$mock = $_ = $this->createCallableMock();
40+
$_->expects(self::once())
4141
->method('__invoke')
4242
->with(self::identicalTo([1, 2, 3]));
4343

@@ -48,9 +48,9 @@ public function shouldResolvePromisesArray(): void
4848
/** @test */
4949
public function shouldResolveSparseArrayInput(): void
5050
{
51-
$mock = $this->createCallableMock();
52-
$mock
53-
->expects(self::once())
51+
/** @var callable $mock */
52+
$mock = $_ = $this->createCallableMock();
53+
$_->expects(self::once())
5454
->method('__invoke')
5555
->with(self::identicalTo([null, 1, null, 1, 1]));
5656

@@ -61,9 +61,9 @@ public function shouldResolveSparseArrayInput(): void
6161
/** @test */
6262
public function shouldResolveValuesGenerator(): void
6363
{
64-
$mock = $this->createCallableMock();
65-
$mock
66-
->expects(self::once())
64+
/** @var callable $mock */
65+
$mock = $_ = $this->createCallableMock();
66+
$_->expects(self::once())
6767
->method('__invoke')
6868
->with(self::identicalTo([1, 2, 3]));
6969

@@ -79,9 +79,9 @@ public function shouldResolveValuesGenerator(): void
7979
/** @test */
8080
public function shouldResolveValuesGeneratorEmpty(): void
8181
{
82-
$mock = $this->createCallableMock();
83-
$mock
84-
->expects(self::once())
82+
/** @var callable $mock */
83+
$mock = $_ = $this->createCallableMock();
84+
$_->expects(self::once())
8585
->method('__invoke')
8686
->with(self::identicalTo([]));
8787

@@ -131,9 +131,9 @@ public function shouldRejectInfiteGeneratorOrRejectedPromises(): void
131131
/** @test */
132132
public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises(): void
133133
{
134-
$mock = $this->createCallableMock();
135-
$mock
136-
->expects(self::once())
134+
/** @var callable $mock */
135+
$mock = $_ = $this->createCallableMock();
136+
$_->expects(self::once())
137137
->method('__invoke')
138138
->with(self::identicalTo([1, 2, 3]));
139139

0 commit comments

Comments
 (0)