Skip to content

Commit 1096398

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 d87b562 commit 1096398

34 files changed

+375
-43
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/.gitattributes export-ignore
22
/.github/ export-ignore
33
/.gitignore export-ignore
4+
/phpstan.legacy.neon.dist export-ignore
45
/phpstan.neon.dist export-ignore
56
/phpunit.xml.dist export-ignore
67
/phpunit.xml.legacy export-ignore

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
name: PHPStan (PHP ${{ matrix.php }})
3636
runs-on: ubuntu-22.04
3737
strategy:
38+
fail-fast: false
3839
matrix:
3940
php:
4041
- 8.2
@@ -52,3 +53,6 @@ jobs:
5253
coverage: none
5354
- run: composer install
5455
- run: vendor/bin/phpstan
56+
if: ${{ matrix.php >= 7.2 }}
57+
- run: vendor/bin/phpstan --configuration="phpstan.legacy.neon.dist"
58+
if: ${{ matrix.php < 7.2 }}

phpstan.legacy.neon.dist

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
- '#PHPDoc tag @param has invalid value \(\?\(callable\(T\): \(Fulfilled\|void\)\) \$onFulfilled\): Unexpected token "\(", expected type at offset 1165#'
6+
- '#Template type Fulfilled of method React\\Promise\\PromiseInterface::then\(\) is not referenced in a parameter.#'
7+
8+
includes:
9+
- phpstan.neon.dist

src/Deferred.php

Lines changed: 10 additions & 2 deletions
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

Lines changed: 15 additions & 3 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)
@@ -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 ($onFulfilled is null ? PromiseInterface<T> : PromiseInterface<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

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

99
/**
1010
* @internal
11+
*
12+
* @template-implements PromiseInterface<never>
13+
* @template-covariant T
1114
*/
1215
final class RejectedPromise implements PromiseInterface
1316
{
@@ -37,6 +40,11 @@ public function __destruct()
3740
\error_log($message);
3841
}
3942

43+
/**
44+
* @template TRejected
45+
* @param ?(callable(\Throwable): (PromiseInterface<TRejected>|TRejected)) $onRejected
46+
* @return ($onRejected is null ? PromiseInterface<T> : PromiseInterface<TRejected>)
47+
*/
4048
public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface
4149
{
4250
if (null === $onRejected) {

src/Promise.php

Lines changed: 16 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[] */
@@ -80,11 +84,11 @@ public function catch(callable $onRejected): PromiseInterface
8084
public function finally(callable $onFulfilledOrRejected): PromiseInterface
8185
{
8286
return $this->then(static function ($value) use ($onFulfilledOrRejected) {
83-
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
87+
return resolve($onFulfilledOrRejected())->then(static function () use ($value) {
8488
return $value;
8589
});
8690
}, static function ($reason) use ($onFulfilledOrRejected) {
87-
return resolve($onFulfilledOrRejected())->then(function () use ($reason) {
91+
return resolve($onFulfilledOrRejected())->then(static function () use ($reason) {
8892
return new RejectedPromise($reason);
8993
});
9094
});
@@ -175,6 +179,10 @@ private function reject(\Throwable $reason): void
175179
$this->settle(reject($reason));
176180
}
177181

182+
/**
183+
* Test out if null can be promise
184+
* @param PromiseInterface<T>|PromiseInterface<never> $result
185+
*/
178186
private function settle(PromiseInterface $result): void
179187
{
180188
$result = $this->unwrap($result);
@@ -207,13 +215,17 @@ private function settle(PromiseInterface $result): void
207215
}
208216
}
209217

218+
/**
219+
* @param PromiseInterface<T>|PromiseInterface<never> $promise
220+
* @return PromiseInterface<T>
221+
*/
210222
private function unwrap(PromiseInterface $promise): PromiseInterface
211223
{
212224
while ($promise instanceof self && null !== $promise->result) {
213225
$promise = $promise->result;
214226
}
215227

216-
return $promise;
228+
return $promise; /** @phpstan-ignore-line */
217229
}
218230

219231
private function call(callable $cb): void

src/PromiseInterface.php

Lines changed: 20 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,18 @@ 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 ($onRejected is null ?
39+
* ($onFulfilled is null ? PromiseInterface<T> : (
40+
* PromiseInterface<(T is never ? T : TFulfilled)>
41+
* )) :
42+
* ($onFulfilled is null ? PromiseInterface<T|TRejected> : (
43+
* PromiseInterface<(T is never ? TRejected : TFulfilled|TRejected)>
44+
* ))
45+
* )
3446
*/
3547
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface;
3648

@@ -44,8 +56,7 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null
4456
* Additionally, you can type hint the `$reason` argument of `$onRejected` to catch
4557
* only specific errors.
4658
*
47-
* @param callable $onRejected
48-
* @return PromiseInterface
59+
* @return PromiseInterface<T>
4960
*/
5061
public function catch(callable $onRejected): PromiseInterface;
5162

@@ -91,8 +102,8 @@ public function catch(callable $onRejected): PromiseInterface;
91102
* ->finally('cleanup');
92103
* ```
93104
*
94-
* @param callable $onFulfilledOrRejected
95-
* @return PromiseInterface
105+
* @param callable(): (mixed|void) $onFulfilledOrRejected
106+
* @return PromiseInterface<T>
96107
*/
97108
public function finally(callable $onFulfilledOrRejected): PromiseInterface;
98109

@@ -118,7 +129,7 @@ public function cancel(): void;
118129
* ```
119130
*
120131
* @param callable $onRejected
121-
* @return PromiseInterface
132+
* @return PromiseInterface<T>
122133
* @deprecated 3.0.0 Use catch() instead
123134
* @see self::catch()
124135
*/
@@ -135,7 +146,7 @@ public function otherwise(callable $onRejected): PromiseInterface;
135146
* ```
136147
*
137148
* @param callable $onFulfilledOrRejected
138-
* @return PromiseInterface
149+
* @return PromiseInterface<T>
139150
* @deprecated 3.0.0 Use finally() instead
140151
* @see self::finally()
141152
*/

src/functions.php

Lines changed: 24 additions & 12 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<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
{
@@ -86,7 +88,11 @@ function all(iterable $promisesOrValues): PromiseInterface
8688
$values[$i] = null;
8789
++$toResolve;
8890

89-
resolve($promiseOrValue)->then(
91+
/**
92+
* @var PromiseInterface<T> $p
93+
*/
94+
$p = resolve($promiseOrValue);
95+
$p->then(
9096
function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void {
9197
$values[$i] = $value;
9298

@@ -119,8 +125,9 @@ function (\Throwable $reason) use (&$continue, $reject): void {
119125
* The returned promise will become **infinitely pending** if `$promisesOrValues`
120126
* contains 0 items.
121127
*
122-
* @param iterable<mixed> $promisesOrValues
123-
* @return PromiseInterface
128+
* @template T
129+
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
130+
* @return PromiseInterface<T>
124131
*/
125132
function race(iterable $promisesOrValues): PromiseInterface
126133
{
@@ -154,8 +161,9 @@ function race(iterable $promisesOrValues): PromiseInterface
154161
* The returned promise will also reject with a `React\Promise\Exception\LengthException`
155162
* if `$promisesOrValues` contains 0 items.
156163
*
157-
* @param iterable<mixed> $promisesOrValues
158-
* @return PromiseInterface
164+
* @template T
165+
* @param iterable<PromiseInterface<T>|T> $promisesOrValues
166+
* @return PromiseInterface<T>
159167
*/
160168
function any(iterable $promisesOrValues): PromiseInterface
161169
{
@@ -170,7 +178,11 @@ function any(iterable $promisesOrValues): PromiseInterface
170178
$cancellationQueue->enqueue($promiseOrValue);
171179
++$toReject;
172180

173-
resolve($promiseOrValue)->then(
181+
/**
182+
* @var PromiseInterface<T> $p
183+
*/
184+
$p = resolve($promiseOrValue);
185+
$p->then(
174186
function ($value) use ($resolve, &$continue): void {
175187
$continue = false;
176188
$resolve($value);

tests/DeferredTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc
5454
gc_collect_cycles();
5555
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
5656

57-
/** @var Deferred $deferred */
5857
$deferred = new Deferred(function () use (&$deferred) {
5958
assert($deferred instanceof Deferred);
6059
});

tests/FunctionAnyTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public function shouldResolveWithAnInputValue(): void
5252
->expects(self::once())
5353
->method('__invoke')
5454
->with(self::identicalTo(1));
55+
assert(is_callable($mock));
5556

5657
any([1, 2, 3])
5758
->then($mock);

tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use function React\Promise\resolve;
1010

1111
require __DIR__ . '/../vendor/autoload.php';
1212

13-
resolve(42)->then('var_dump');
13+
/** @var callable $callable */
14+
$callable = 'var_dump';
15+
resolve(42)->then($callable);
1416

1517
?>
1618
--EXPECT--

tests/Internal/CancellationQueueTest.php

Lines changed: 3 additions & 0 deletions
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)