From b14580de7ca6d2374490b96a27ecd6fc5c55a9c8 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Tue, 11 Jul 2023 09:46:17 +0200 Subject: [PATCH] [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`. ```php $f = function (): int|string { return time() % 2 ? 'string' : time(); }; /** * @return PromiseInterface */ $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 https://github.com/reactphp/promise/pull/246 and https://github.com/reactphp/promise/pull/188 and is a requirement for https://github.com/reactphp/async/pull/40 --- .github/workflows/ci.yml | 1 - src/Deferred.php | 12 ++- src/Internal/FulfilledPromise.php | 18 +++- src/Internal/RejectedPromise.php | 17 ++++ src/Promise.php | 23 ++++- src/PromiseInterface.php | 31 ++++--- src/functions.php | 26 +++--- tests/DeferredTest.php | 8 +- ...ReportUnhandledForTypeErrorOnlyOnPhp7.phpt | 2 +- ...ReportUnhandledForTypeErrorOnlyOnPhp8.phpt | 2 +- tests/Internal/CancellationQueueTest.php | 3 + tests/Internal/FulfilledPromiseTest.php | 8 +- tests/Internal/RejectedPromiseTest.php | 3 + .../PromiseAdapter/CallbackPromiseAdapter.php | 9 +- .../PromiseAdapterInterface.php | 12 ++- tests/PromiseTest.php | 12 ++- .../PromiseTest/PromiseFulfilledTestTrait.php | 10 ++- .../PromiseTest/PromiseRejectedTestTrait.php | 9 +- tests/PromiseTest/RejectTestTrait.php | 7 +- tests/PromiseTest/ResolveTestTrait.php | 5 +- tests/types/all.php | 10 +++ tests/types/any.php | 10 +++ tests/types/deferred.php | 12 +++ tests/types/race.php | 10 +++ tests/types/reject.php | 59 +++++++++++++ tests/types/resolve.php | 87 +++++++++++++++++++ 26 files changed, 355 insertions(+), 51 deletions(-) create mode 100644 tests/types/all.php create mode 100644 tests/types/any.php create mode 100644 tests/types/deferred.php create mode 100644 tests/types/race.php create mode 100644 tests/types/reject.php create mode 100644 tests/types/resolve.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43c9cb3f..40fdcf94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,6 @@ jobs: - 7.4 - 7.3 - 7.2 - - 7.1 steps: - uses: actions/checkout@v3 - uses: shivammathur/setup-php@v2 diff --git a/src/Deferred.php b/src/Deferred.php index 82f66dad..53d945e4 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -2,9 +2,14 @@ namespace React\Promise; +/** + * @template T + */ final class Deferred { - /** @var Promise */ + /** + * @var PromiseInterface + */ private $promise; /** @var callable */ @@ -21,13 +26,16 @@ public function __construct(callable $canceller = null) }, $canceller); } + /** + * @return PromiseInterface + */ public function promise(): PromiseInterface { return $this->promise; } /** - * @param mixed $value + * @param T $value */ public function resolve($value): void { diff --git a/src/Internal/FulfilledPromise.php b/src/Internal/FulfilledPromise.php index 0712f763..3c02b434 100644 --- a/src/Internal/FulfilledPromise.php +++ b/src/Internal/FulfilledPromise.php @@ -7,14 +7,17 @@ /** * @internal + * + * @template T + * @template-implements PromiseInterface */ final class FulfilledPromise implements PromiseInterface { - /** @var mixed */ + /** @var T */ private $value; /** - * @param mixed $value + * @param T $value * @throws \InvalidArgumentException */ public function __construct($value = null) @@ -26,6 +29,11 @@ public function __construct($value = null) $this->value = $value; } + /** + * @template TFulfilled + * @param ?(callable((T is void ? null : T)): (PromiseInterface|TFulfilled)) $onFulfilled + * @return PromiseInterface<($onFulfilled is null ? T : TFulfilled)> + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null === $onFulfilled) { @@ -33,7 +41,11 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): } try { - return resolve($onFulfilled($this->value)); + /** + * @var PromiseInterface|T $result + */ + $result = $onFulfilled($this->value); + return resolve($result); } catch (\Throwable $exception) { return new RejectedPromise($exception); } diff --git a/src/Internal/RejectedPromise.php b/src/Internal/RejectedPromise.php index a29cc92d..fb980f43 100644 --- a/src/Internal/RejectedPromise.php +++ b/src/Internal/RejectedPromise.php @@ -8,6 +8,8 @@ /** * @internal + * + * @template-implements PromiseInterface */ final class RejectedPromise implements PromiseInterface { @@ -37,6 +39,12 @@ public function __destruct() \error_log($message); } + /** + * @template TRejected + * @param ?callable $onFulfilled + * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected + * @return PromiseInterface<($onRejected is null ? never : TRejected)> + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null === $onRejected) { @@ -52,12 +60,21 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): } } + /** + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface + */ public function catch(callable $onRejected): PromiseInterface { if (!_checkTypehint($onRejected, $this->reason)) { return $this; } + /** + * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected + */ return $this->then(null, $onRejected); } diff --git a/src/Promise.php b/src/Promise.php index 819e414a..1613db51 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -4,12 +4,16 @@ use React\Promise\Internal\RejectedPromise; +/** + * @template T + * @template-implements PromiseInterface + */ final class Promise implements PromiseInterface { /** @var ?callable */ private $canceller; - /** @var ?PromiseInterface */ + /** @var ?PromiseInterface */ private $result; /** @var callable[] */ @@ -66,6 +70,12 @@ static function () use (&$parent) { ); } + /** + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface + */ public function catch(callable $onRejected): PromiseInterface { return $this->then(null, static function ($reason) use ($onRejected) { @@ -73,6 +83,9 @@ public function catch(callable $onRejected): PromiseInterface return new RejectedPromise($reason); } + /** + * @var callable(\Throwable):(PromiseInterface|TRejected) $onRejected + */ return $onRejected($reason); }); } @@ -175,6 +188,9 @@ private function reject(\Throwable $reason): void $this->settle(reject($reason)); } + /** + * @param PromiseInterface $result + */ private function settle(PromiseInterface $result): void { $result = $this->unwrap($result); @@ -207,9 +223,14 @@ private function settle(PromiseInterface $result): void } } + /** + * @param PromiseInterface $promise + * @return PromiseInterface + */ private function unwrap(PromiseInterface $promise): PromiseInterface { while ($promise instanceof self && null !== $promise->result) { + /** @var PromiseInterface $promise */ $promise = $promise->result; } diff --git a/src/PromiseInterface.php b/src/PromiseInterface.php index 47117072..5869f76b 100644 --- a/src/PromiseInterface.php +++ b/src/PromiseInterface.php @@ -2,6 +2,9 @@ namespace React\Promise; +/** + * @template-covariant T + */ interface PromiseInterface { /** @@ -28,9 +31,11 @@ interface PromiseInterface * 2. `$onFulfilled` and `$onRejected` will never be called more * than once. * - * @param callable|null $onFulfilled - * @param callable|null $onRejected - * @return PromiseInterface + * @template TFulfilled + * @template TRejected + * @param ?(callable((T is void ? null : T)): (PromiseInterface|TFulfilled)) $onFulfilled + * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected + * @return PromiseInterface<($onRejected is null ? ($onFulfilled is null ? T : TFulfilled) : ($onFulfilled is null ? T|TRejected : TFulfilled|TRejected))> */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; @@ -44,8 +49,10 @@ public function then(?callable $onFulfilled = null, ?callable $onRejected = null * Additionally, you can type hint the `$reason` argument of `$onRejected` to catch * only specific errors. * - * @param callable $onRejected - * @return PromiseInterface + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface */ public function catch(callable $onRejected): PromiseInterface; @@ -91,8 +98,8 @@ public function catch(callable $onRejected): PromiseInterface; * ->finally('cleanup'); * ``` * - * @param callable $onFulfilledOrRejected - * @return PromiseInterface + * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected + * @return PromiseInterface */ public function finally(callable $onFulfilledOrRejected): PromiseInterface; @@ -117,8 +124,10 @@ public function cancel(): void; * $promise->catch($onRejected); * ``` * - * @param callable $onRejected - * @return PromiseInterface + * @template TThrowable of \Throwable + * @template TRejected + * @param callable(TThrowable): (PromiseInterface|TRejected) $onRejected + * @return PromiseInterface * @deprecated 3.0.0 Use catch() instead * @see self::catch() */ @@ -134,8 +143,8 @@ public function otherwise(callable $onRejected): PromiseInterface; * $promise->finally($onFulfilledOrRejected); * ``` * - * @param callable $onFulfilledOrRejected - * @return PromiseInterface + * @param callable(): (void|PromiseInterface) $onFulfilledOrRejected + * @return PromiseInterface * @deprecated 3.0.0 Use finally() instead * @see self::finally() */ diff --git a/src/functions.php b/src/functions.php index c8107f8d..73b6334d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -17,8 +17,9 @@ * * If `$promiseOrValue` is a promise, it will be returned as is. * - * @param mixed $promiseOrValue - * @return PromiseInterface + * @template T + * @param PromiseInterface|T $promiseOrValue + * @return PromiseInterface */ function resolve($promiseOrValue): PromiseInterface { @@ -31,6 +32,7 @@ function resolve($promiseOrValue): PromiseInterface if (\method_exists($promiseOrValue, 'cancel')) { $canceller = [$promiseOrValue, 'cancel']; + assert(\is_callable($canceller)); } return new Promise(function ($resolve, $reject) use ($promiseOrValue): void { @@ -54,8 +56,7 @@ function resolve($promiseOrValue): PromiseInterface * throwing an exception. For example, it allows you to propagate a rejection with * the value of another promise. * - * @param \Throwable $reason - * @return PromiseInterface + * @return PromiseInterface */ function reject(\Throwable $reason): PromiseInterface { @@ -68,8 +69,9 @@ function reject(\Throwable $reason): PromiseInterface * will be an array containing the resolution values of each of the items in * `$promisesOrValues`. * - * @param iterable $promisesOrValues - * @return PromiseInterface + * @template T + * @param iterable|T> $promisesOrValues + * @return PromiseInterface> */ function all(iterable $promisesOrValues): PromiseInterface { @@ -119,14 +121,15 @@ function (\Throwable $reason) use (&$continue, $reject): void { * The returned promise will become **infinitely pending** if `$promisesOrValues` * contains 0 items. * - * @param iterable $promisesOrValues - * @return PromiseInterface + * @template T + * @param iterable|T> $promisesOrValues + * @return PromiseInterface */ function race(iterable $promisesOrValues): PromiseInterface { $cancellationQueue = new Internal\CancellationQueue(); - return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void { $continue = true; foreach ($promisesOrValues as $promiseOrValue) { @@ -154,8 +157,9 @@ function race(iterable $promisesOrValues): PromiseInterface * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains 0 items. * - * @param iterable $promisesOrValues - * @return PromiseInterface + * @template T + * @param iterable|T> $promisesOrValues + * @return PromiseInterface */ function any(iterable $promisesOrValues): PromiseInterface { diff --git a/tests/DeferredTest.php b/tests/DeferredTest.php index 186ed1d7..416254f5 100644 --- a/tests/DeferredTest.php +++ b/tests/DeferredTest.php @@ -4,10 +4,16 @@ use React\Promise\PromiseAdapter\CallbackPromiseAdapter; +/** + * @template T + */ class DeferredTest extends TestCase { use PromiseTest\FullTestTrait; + /** + * @return CallbackPromiseAdapter + */ public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter { $d = new Deferred($canceller); @@ -54,7 +60,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc gc_collect_cycles(); gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on - /** @var Deferred $deferred */ + /** @var Deferred $deferred */ $deferred = new Deferred(function () use (&$deferred) { assert($deferred instanceof Deferred); }); diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt index 7a0cf9a1..d4880ce0 100644 --- a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt +++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp7.phpt @@ -12,7 +12,7 @@ use function React\Promise\reject; require __DIR__ . '/../vendor/autoload.php'; -reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { +reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line echo 'This will never be shown because the types do not match' . PHP_EOL; }); diff --git a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt index c8694f34..bd75cdae 100644 --- a/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt +++ b/tests/FunctionRejectTestThenMismatchThrowsTypeErrorAndShouldReportUnhandledForTypeErrorOnlyOnPhp8.phpt @@ -12,7 +12,7 @@ use function React\Promise\reject; require __DIR__ . '/../vendor/autoload.php'; -reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { +reject(new RuntimeException('foo'))->then(null, function (UnexpectedValueException $unexpected): void { // @phpstan-ignore-line echo 'This will never be shown because the types do not match' . PHP_EOL; }); diff --git a/tests/Internal/CancellationQueueTest.php b/tests/Internal/CancellationQueueTest.php index c2907f73..fea5696f 100644 --- a/tests/Internal/CancellationQueueTest.php +++ b/tests/Internal/CancellationQueueTest.php @@ -96,6 +96,9 @@ public function rethrowsExceptionsThrownFromCancel(): void $cancellationQueue(); } + /** + * @return Deferred + */ private function getCancellableDeferred(): Deferred { return new Deferred($this->expectCallableOnce()); diff --git a/tests/Internal/FulfilledPromiseTest.php b/tests/Internal/FulfilledPromiseTest.php index 073b9d7a..5a9ca9cd 100644 --- a/tests/Internal/FulfilledPromiseTest.php +++ b/tests/Internal/FulfilledPromiseTest.php @@ -9,14 +9,20 @@ use React\Promise\PromiseTest\PromiseSettledTestTrait; use React\Promise\TestCase; +/** + * @template T + */ class FulfilledPromiseTest extends TestCase { use PromiseSettledTestTrait, PromiseFulfilledTestTrait; + /** + * @return CallbackPromiseAdapter + */ public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter { - /** @var ?FulfilledPromise */ + /** @var ?FulfilledPromise */ $promise = null; return new CallbackPromiseAdapter([ diff --git a/tests/Internal/RejectedPromiseTest.php b/tests/Internal/RejectedPromiseTest.php index 72cef091..7cfc0d66 100644 --- a/tests/Internal/RejectedPromiseTest.php +++ b/tests/Internal/RejectedPromiseTest.php @@ -14,6 +14,9 @@ class RejectedPromiseTest extends TestCase use PromiseSettledTestTrait, PromiseRejectedTestTrait; + /** + * @return CallbackPromiseAdapter + */ public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter { /** @var ?RejectedPromise */ diff --git a/tests/PromiseAdapter/CallbackPromiseAdapter.php b/tests/PromiseAdapter/CallbackPromiseAdapter.php index 14a0acd4..9b72204c 100644 --- a/tests/PromiseAdapter/CallbackPromiseAdapter.php +++ b/tests/PromiseAdapter/CallbackPromiseAdapter.php @@ -4,6 +4,10 @@ use React\Promise\PromiseInterface; +/** + * @template T + * @template-implements PromiseAdapterInterface + */ class CallbackPromiseAdapter implements PromiseAdapterInterface { /** @var callable[] */ @@ -17,12 +21,15 @@ public function __construct(array $callbacks) $this->callbacks = $callbacks; } + /** + * @return PromiseInterface + */ public function promise(): PromiseInterface { return ($this->callbacks['promise'])(...func_get_args()); } - public function resolve(): void + public function resolve($value): void { ($this->callbacks['resolve'])(...func_get_args()); } diff --git a/tests/PromiseAdapter/PromiseAdapterInterface.php b/tests/PromiseAdapter/PromiseAdapterInterface.php index 727fd514..171d03e3 100644 --- a/tests/PromiseAdapter/PromiseAdapterInterface.php +++ b/tests/PromiseAdapter/PromiseAdapterInterface.php @@ -4,10 +4,20 @@ use React\Promise\PromiseInterface; +/** + * @template T + */ interface PromiseAdapterInterface { + /** + * @return PromiseInterface + */ public function promise(): PromiseInterface; - public function resolve(): void; + + /** + * @param mixed $value + */ + public function resolve($value): void; public function reject(): void; public function settle(): void; } diff --git a/tests/PromiseTest.php b/tests/PromiseTest.php index 06d89eb1..74a1446c 100644 --- a/tests/PromiseTest.php +++ b/tests/PromiseTest.php @@ -5,10 +5,16 @@ use Exception; use React\Promise\PromiseAdapter\CallbackPromiseAdapter; +/** + * @template T + */ class PromiseTest extends TestCase { use PromiseTest\FullTestTrait; + /** + * @return CallbackPromiseAdapter + */ public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter { $resolveCallback = $rejectCallback = null; @@ -146,7 +152,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException(): void { gc_collect_cycles(); - /** @var Promise $promise */ + /** @var Promise $promise */ $promise = new Promise(function () {}, function () use (&$promise) { assert($promise instanceof Promise); throw new \Exception('foo'); @@ -165,7 +171,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException(): void { gc_collect_cycles(); - /** @var Promise $promise */ + /** @var Promise $promise */ $promise = new Promise(function () use (&$promise) { assert($promise instanceof Promise); throw new \Exception('foo'); @@ -186,7 +192,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException(): void { gc_collect_cycles(); - /** @var Promise $promise */ + /** @var Promise $promise */ $promise = new Promise(function () { throw new \Exception('foo'); }, function () use (&$promise) { diff --git a/tests/PromiseTest/PromiseFulfilledTestTrait.php b/tests/PromiseTest/PromiseFulfilledTestTrait.php index d982214a..c5f35916 100644 --- a/tests/PromiseTest/PromiseFulfilledTestTrait.php +++ b/tests/PromiseTest/PromiseFulfilledTestTrait.php @@ -4,6 +4,7 @@ use Exception; use React\Promise\PromiseAdapter\PromiseAdapterInterface; +use React\Promise\PromiseInterface; use stdClass; use function React\Promise\reject; use function React\Promise\resolve; @@ -187,6 +188,7 @@ public function thenShouldSwitchFromCallbacksToErrbacksWhenCallbackThrows(): voi */ public function thenShouldContinueToExecuteCallbacksWhenPriorCallbackSuspendsFiber(): void { + /** @var PromiseAdapterInterface $adapter */ $adapter = $this->getPromiseTestAdapter(); $adapter->resolve(42); @@ -261,7 +263,7 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFul $adapter->resolve($value); $adapter->promise() - ->finally(function () { + ->finally(function (): int { // @phpstan-ignore-line return 1; }) ->then($mock); @@ -282,7 +284,7 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfil $adapter->resolve($value); $adapter->promise() - ->finally(function () { + ->finally(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then($mock); @@ -382,7 +384,7 @@ public function alwaysShouldNotSuppressValueWhenHandlerReturnsANonPromiseForFulf $adapter->resolve($value); $adapter->promise() - ->always(function () { + ->always(function (): int { // @phpstan-ignore-line return 1; }) ->then($mock); @@ -406,7 +408,7 @@ public function alwaysShouldNotSuppressValueWhenHandlerReturnsAPromiseForFulfill $adapter->resolve($value); $adapter->promise() - ->always(function () { + ->always(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then($mock); diff --git a/tests/PromiseTest/PromiseRejectedTestTrait.php b/tests/PromiseTest/PromiseRejectedTestTrait.php index 12287ce8..0f80d727 100644 --- a/tests/PromiseTest/PromiseRejectedTestTrait.php +++ b/tests/PromiseTest/PromiseRejectedTestTrait.php @@ -5,6 +5,7 @@ use Exception; use InvalidArgumentException; use React\Promise\PromiseAdapter\PromiseAdapterInterface; +use React\Promise\PromiseInterface; use function React\Promise\reject; use function React\Promise\resolve; @@ -298,7 +299,7 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseFo $adapter->reject($exception); $adapter->promise() - ->finally(function () { + ->finally(function (): int { // @phpstan-ignore-line return 1; }) ->then(null, $mock); @@ -319,7 +320,7 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRe $adapter->reject($exception); $adapter->promise() - ->finally(function () { + ->finally(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then(null, $mock); @@ -504,7 +505,7 @@ public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsANonPromiseFor $adapter->reject($exception); $adapter->promise() - ->finally(function () { + ->finally(function (): int { // @phpstan-ignore-line return 1; }) ->then(null, $mock); @@ -528,7 +529,7 @@ public function alwaysShouldNotSuppressRejectionWhenHandlerReturnsAPromiseForRej $adapter->reject($exception); $adapter->promise() - ->always(function () { + ->always(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then(null, $mock); diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index ad55ca28..2b5617bb 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/PromiseTest/RejectTestTrait.php @@ -6,6 +6,7 @@ use React\Promise; use React\Promise\Deferred; use React\Promise\PromiseAdapter\PromiseAdapterInterface; +use React\Promise\PromiseInterface; use function React\Promise\reject; use function React\Promise\resolve; @@ -73,7 +74,7 @@ public function rejectShouldMakePromiseImmutable(): void ->with($this->identicalTo($exception1)); $adapter->promise() - ->then(null, function ($value) use ($exception3, $adapter) { + ->then(null, function (\Throwable $value) use ($exception3, $adapter): PromiseInterface { $adapter->reject($exception3); return reject($value); @@ -140,7 +141,7 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsANonPromise() ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () { + ->finally(function (): int { // @phpstan-ignore-line return 1; }) ->then(null, $mock); @@ -162,7 +163,7 @@ public function finallyShouldNotSuppressRejectionWhenHandlerReturnsAPromise(): v ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () { + ->finally(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then(null, $mock); diff --git a/tests/PromiseTest/ResolveTestTrait.php b/tests/PromiseTest/ResolveTestTrait.php index 357fdedc..ed73dfc8 100644 --- a/tests/PromiseTest/ResolveTestTrait.php +++ b/tests/PromiseTest/ResolveTestTrait.php @@ -6,6 +6,7 @@ use LogicException; use React\Promise; use React\Promise\PromiseAdapter\PromiseAdapterInterface; +use React\Promise\PromiseInterface; use stdClass; use function React\Promise\reject; use function React\Promise\resolve; @@ -200,7 +201,7 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsANonPromise(): vo ->with($this->identicalTo($value)); $adapter->promise() - ->finally(function () { + ->finally(function (): int { // @phpstan-ignore-line return 1; }) ->then($mock); @@ -222,7 +223,7 @@ public function finallyShouldNotSuppressValueWhenHandlerReturnsAPromise(): void ->with($this->identicalTo($value)); $adapter->promise() - ->finally(function () { + ->finally(function (): PromiseInterface { // @phpstan-ignore-line return resolve(1); }) ->then($mock); diff --git a/tests/types/all.php b/tests/types/all.php new file mode 100644 index 00000000..b02b9f1f --- /dev/null +++ b/tests/types/all.php @@ -0,0 +1,10 @@ +>', all([resolve(true), resolve(false)])); +assertType('React\Promise\PromiseInterface>', all([resolve(true), false])); +assertType('React\Promise\PromiseInterface>', all([true, time()])); +assertType('React\Promise\PromiseInterface>', all([resolve(true), resolve(time())])); diff --git a/tests/types/any.php b/tests/types/any.php new file mode 100644 index 00000000..2182dd37 --- /dev/null +++ b/tests/types/any.php @@ -0,0 +1,10 @@ +', any([resolve(true), resolve(false)])); +assertType('React\Promise\PromiseInterface', any([resolve(true), false])); +assertType('React\Promise\PromiseInterface', any([true, time()])); +assertType('React\Promise\PromiseInterface', any([resolve(true), resolve(time())])); diff --git a/tests/types/deferred.php b/tests/types/deferred.php new file mode 100644 index 00000000..ffdea720 --- /dev/null +++ b/tests/types/deferred.php @@ -0,0 +1,12 @@ +', $deferredA->promise()); + +/** @var Deferred $deferredB */ +$deferredB = new Deferred(); +$deferredB->resolve(42); +assertType('React\Promise\PromiseInterface', $deferredB->promise()); diff --git a/tests/types/race.php b/tests/types/race.php new file mode 100644 index 00000000..fa1a8616 --- /dev/null +++ b/tests/types/race.php @@ -0,0 +1,10 @@ +', race([resolve(true), resolve(false)])); +assertType('React\Promise\PromiseInterface', race([resolve(true), false])); +assertType('React\Promise\PromiseInterface', race([true, time()])); +assertType('React\Promise\PromiseInterface', race([resolve(true), resolve(time())])); diff --git a/tests/types/reject.php b/tests/types/reject.php new file mode 100644 index 00000000..cc443de4 --- /dev/null +++ b/tests/types/reject.php @@ -0,0 +1,59 @@ +', reject(new RuntimeException())); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->then(null, null)); +// assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->then(function (): int { +// return 42; +// })); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, function (): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(null, function (): PromiseInterface { + return resolve(42); +})); +// assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(function (): bool { +// return true; +// }, function (): int { +// return 42; +// })); + +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (\UnexpectedValueException $e): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->catch(function (): PromiseInterface { + return resolve(42); +})); + +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): void { })); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): never { + throw new \UnexpectedValueException(); +})); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->finally(function (): PromiseInterface { + return reject(new \UnexpectedValueException()); +})); + +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (\UnexpectedValueException $e): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->otherwise(function (): PromiseInterface { + return resolve(42); +})); + +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): void { })); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): never { + throw new \UnexpectedValueException(); +})); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new RuntimeException())->always(function (): PromiseInterface { + return reject(new \UnexpectedValueException()); +})); diff --git a/tests/types/resolve.php b/tests/types/resolve.php new file mode 100644 index 00000000..e50272f0 --- /dev/null +++ b/tests/types/resolve.php @@ -0,0 +1,87 @@ + + */ +function stringOrIntPromise(): PromiseInterface { + return resolve(time() % 2 ? 'string' : time()); +}; + +assertType('React\Promise\PromiseInterface', resolve(true)); +assertType('React\Promise\PromiseInterface', resolve(stringOrInt())); +assertType('React\Promise\PromiseInterface', stringOrIntPromise()); +assertType('React\Promise\PromiseInterface', resolve(resolve(true))); + +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, null)); +assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $bool): bool { + return $bool; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $value): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $value): PromiseInterface { + return resolve(42); +})); +assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->then(function (bool $value): never { + throw new \RuntimeException(); +})); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, function (\Throwable $e): int { + return 42; +})); + +assertType('React\Promise\PromiseInterface', resolve(true)->then(function (bool $bool): void { })); +assertType('React\Promise\PromiseInterface', resolve(false)->then(function (bool $bool): void { })->then(function (null $value) { })); + +$value = null; +assertType('React\Promise\PromiseInterface', resolve(true)->then(static function (bool $v) use (&$value): void { + $value = $v; +})); +assertType('bool|null', $value); + +assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): never { + throw $e; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->catch(function (\Throwable $e): PromiseInterface { + return resolve(42); +})); + +assertType('React\Promise\PromiseInterface', resolve(true)->finally(function (): void { })); +// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->finally(function (): never { +// throw new \RuntimeException(); +// })); +// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->finally(function (): PromiseInterface { +// return reject(new \RuntimeException()); +// })); + +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): never { + throw $e; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): int { + return 42; +})); +assertType('React\Promise\PromiseInterface', resolve(true)->otherwise(function (\Throwable $e): PromiseInterface { + return resolve(42); +})); + +assertType('React\Promise\PromiseInterface', resolve(true)->always(function (): void { })); +// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->always(function (): never { +// throw new \RuntimeException(); +// })); +// assertType('React\Promise\PromiseInterface<*NEVER*>', resolve(true)->always(function (): PromiseInterface { +// return reject(new \RuntimeException()); +// }));