diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43c9cb3f..e6939582 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: name: PHPStan (PHP ${{ matrix.php }}) runs-on: ubuntu-22.04 strategy: + fail-fast: false matrix: php: - 8.2 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 895c8410..0e153421 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -4,3 +4,4 @@ parameters: paths: - src/ - tests/ + - types/ diff --git a/src/Deferred.php b/src/Deferred.php index 82f66dad..28ab18bf 100644 --- a/src/Deferred.php +++ b/src/Deferred.php @@ -2,9 +2,14 @@ namespace React\Promise; +/** + * @template-covariant T + */ final class Deferred { - /** @var Promise */ + /** + * @var PromiseInterface + */ private $promise; /** @var callable */ @@ -21,6 +26,9 @@ public function __construct(callable $canceller = null) }, $canceller); } + /** + * @return PromiseInterface + */ public function promise(): PromiseInterface { return $this->promise; diff --git a/src/Internal/FulfilledPromise.php b/src/Internal/FulfilledPromise.php index 0712f763..7806b46a 100644 --- a/src/Internal/FulfilledPromise.php +++ b/src/Internal/FulfilledPromise.php @@ -7,14 +7,17 @@ /** * @internal + * + * @template-implements PromiseInterface + * @template-covariant T */ 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,9 @@ public function __construct($value = null) $this->value = $value; } + /** + * @inheritdoc + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null === $onFulfilled) { @@ -39,11 +45,17 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): } } + /** + * @inheritdoc + */ public function catch(callable $onRejected): PromiseInterface { return $this; } + /** + * @inheritdoc + */ public function finally(callable $onFulfilledOrRejected): PromiseInterface { return $this->then(function ($value) use ($onFulfilledOrRejected): PromiseInterface { @@ -60,6 +72,7 @@ public function cancel(): void /** * @deprecated 3.0.0 Use `catch()` instead * @see self::catch() + * @inheritdoc */ public function otherwise(callable $onRejected): PromiseInterface { @@ -69,6 +82,7 @@ public function otherwise(callable $onRejected): PromiseInterface /** * @deprecated 3.0.0 Use `finally()` instead * @see self::finally() + * @inheritdoc */ public function always(callable $onFulfilledOrRejected): PromiseInterface { diff --git a/src/Internal/RejectedPromise.php b/src/Internal/RejectedPromise.php index cbd8ef53..7802aa4e 100644 --- a/src/Internal/RejectedPromise.php +++ b/src/Internal/RejectedPromise.php @@ -8,6 +8,9 @@ /** * @internal + * + * @template-implements PromiseInterface + * @template-covariant T */ final class RejectedPromise implements PromiseInterface { @@ -22,6 +25,9 @@ public function __construct(\Throwable $reason) $this->reason = $reason; } + /** + * @inheritdoc + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null === $onRejected) { @@ -35,6 +41,9 @@ public function then(callable $onFulfilled = null, callable $onRejected = null): } } + /** + * @inheritdoc + */ public function catch(callable $onRejected): PromiseInterface { if (!_checkTypehint($onRejected, $this->reason)) { @@ -44,6 +53,9 @@ public function catch(callable $onRejected): PromiseInterface return $this->then(null, $onRejected); } + /** + * @inheritdoc + */ public function finally(callable $onFulfilledOrRejected): PromiseInterface { return $this->then(null, function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface { @@ -60,6 +72,7 @@ public function cancel(): void /** * @deprecated 3.0.0 Use `catch()` instead * @see self::catch() + * @inheritdoc */ public function otherwise(callable $onRejected): PromiseInterface { @@ -69,6 +82,7 @@ public function otherwise(callable $onRejected): PromiseInterface /** * @deprecated 3.0.0 Use `always()` instead * @see self::always() + * @inheritdoc */ public function always(callable $onFulfilledOrRejected): PromiseInterface { diff --git a/src/Promise.php b/src/Promise.php index a2d72b6d..e25a5ec4 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -3,13 +3,18 @@ namespace React\Promise; use React\Promise\Internal\RejectedPromise; +use Throwable; +/** + * @template-implements PromiseInterface + * @template-covariant T + */ final class Promise implements PromiseInterface { /** @var ?callable */ private $canceller; - /** @var ?PromiseInterface */ + /** @var PromiseInterface */ private $result; /** @var callable[] */ @@ -30,6 +35,9 @@ public function __construct(callable $resolver, callable $canceller = null) $this->call($cb); } + /** + * @inheritdoc + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null !== $this->result) { @@ -63,6 +71,9 @@ static function () use (&$parent) { ); } + /** + * @inheritdoc + */ public function catch(callable $onRejected): PromiseInterface { return $this->then(null, static function ($reason) use ($onRejected) { @@ -74,14 +85,17 @@ public function catch(callable $onRejected): PromiseInterface }); } + /** + * @inheritdoc + */ public function finally(callable $onFulfilledOrRejected): PromiseInterface { return $this->then(static function ($value) use ($onFulfilledOrRejected) { - return resolve($onFulfilledOrRejected())->then(function () use ($value) { + return resolve($onFulfilledOrRejected())->then(function ($_) use ($value) { return $value; }); }, static function ($reason) use ($onFulfilledOrRejected) { - return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return resolve($onFulfilledOrRejected())->then(function ($_) use ($reason) { return new RejectedPromise($reason); }); }); @@ -125,6 +139,7 @@ public function cancel(): void /** * @deprecated 3.0.0 Use `catch()` instead * @see self::catch() + * @inheritdoc */ public function otherwise(callable $onRejected): PromiseInterface { @@ -134,6 +149,7 @@ public function otherwise(callable $onRejected): PromiseInterface /** * @deprecated 3.0.0 Use `finally()` instead * @see self::finally() + * @inheritdoc */ public function always(callable $onFulfilledOrRejected): PromiseInterface { @@ -157,7 +173,7 @@ private function resolver(callable $onFulfilled = null, callable $onRejected = n }; } - private function reject(\Throwable $reason): void + private function reject(Throwable $reason): void { if (null !== $this->result) { return; @@ -166,6 +182,9 @@ private function reject(\Throwable $reason): void $this->settle(reject($reason)); } + /** + * @param PromiseInterface|PromiseInterface $result + */ private function settle(PromiseInterface $result): void { $result = $this->unwrap($result); @@ -193,13 +212,17 @@ private function settle(PromiseInterface $result): void } } + /** + * @param PromiseInterface|PromiseInterface $promise + * @return PromiseInterface + */ private function unwrap(PromiseInterface $promise): PromiseInterface { while ($promise instanceof self && null !== $promise->result) { $promise = $promise->result; } - return $promise; + return $promise; /** @phpstan-ignore-line */ } private function call(callable $cb): void @@ -245,7 +268,7 @@ static function ($value) use (&$target) { $target = null; } }, - static function (\Throwable $reason) use (&$target) { + static function (Throwable $reason) use (&$target) { if ($target !== null) { $target->reject($reason); $target = null; @@ -253,7 +276,7 @@ static function (\Throwable $reason) use (&$target) { } ); } - } catch (\Throwable $e) { + } catch (Throwable $e) { $target = null; $this->reject($e); } diff --git a/src/PromiseInterface.php b/src/PromiseInterface.php index 47117072..297f1f97 100644 --- a/src/PromiseInterface.php +++ b/src/PromiseInterface.php @@ -2,6 +2,9 @@ namespace React\Promise; +/** + * @template-covariant T + */ interface PromiseInterface { /** @@ -28,9 +31,9 @@ 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 as PromiseInterface|T + * @param (callable(T): TFulfilled)|null $onFulfilled + * @return PromiseInterface */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; @@ -44,8 +47,7 @@ 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 + * @return PromiseInterface */ public function catch(callable $onRejected): PromiseInterface; @@ -91,8 +93,9 @@ public function catch(callable $onRejected): PromiseInterface; * ->finally('cleanup'); * ``` * - * @param callable $onFulfilledOrRejected - * @return PromiseInterface + * @template TReturn of PromiseInterface|T + * @param callable(T=): TReturn $onFulfilledOrRejected + * @return PromiseInterface */ public function finally(callable $onFulfilledOrRejected): PromiseInterface; @@ -118,7 +121,7 @@ public function cancel(): void; * ``` * * @param callable $onRejected - * @return PromiseInterface + * @return PromiseInterface * @deprecated 3.0.0 Use catch() instead * @see self::catch() */ @@ -135,7 +138,7 @@ public function otherwise(callable $onRejected): PromiseInterface; * ``` * * @param callable $onFulfilledOrRejected - * @return PromiseInterface + * @return PromiseInterface * @deprecated 3.0.0 Use finally() instead * @see self::finally() */ diff --git a/src/functions.php b/src/functions.php index c42b715e..92a3d355 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 { @@ -30,7 +31,9 @@ function resolve($promiseOrValue): PromiseInterface $canceller = null; if (\method_exists($promiseOrValue, 'cancel')) { - $canceller = [$promiseOrValue, 'cancel']; + $canceller = static function () use ($promiseOrValue) { + $promiseOrValue->cancel(); + }; } return new Promise(function ($resolve, $reject) use ($promiseOrValue): void { @@ -54,8 +57,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 +70,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 array|T> $promisesOrValues + * @return PromiseInterface> */ function all(iterable $promisesOrValues): PromiseInterface { @@ -87,7 +90,7 @@ function all(iterable $promisesOrValues): PromiseInterface ++$toResolve; resolve($promiseOrValue)->then( - function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { + function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { /** @phpstan-ignore-line */ $values[$i] = $value; if (0 === --$toResolve && !$continue) { @@ -119,8 +122,9 @@ 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 array|T> $promisesOrValues + * @return PromiseInterface */ function race(iterable $promisesOrValues): PromiseInterface { @@ -132,7 +136,7 @@ function race(iterable $promisesOrValues): PromiseInterface foreach ($promisesOrValues as $promiseOrValue) { $cancellationQueue->enqueue($promiseOrValue); - resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void { + resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void { /** @phpstan-ignore-line */ $continue = false; }); @@ -154,8 +158,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 array|T> $promisesOrValues + * @return PromiseInterface */ function any(iterable $promisesOrValues): PromiseInterface { @@ -171,7 +176,7 @@ function any(iterable $promisesOrValues): PromiseInterface ++$toReject; resolve($promiseOrValue)->then( - function ($value) use ($resolve, &$continue): void { + function ($value) use ($resolve, &$continue): void { /** @phpstan-ignore-line */ $continue = false; $resolve($value); }, diff --git a/tests/Internal/CancellationQueueTest.php b/tests/Internal/CancellationQueueTest.php index c2907f73..3d802540 100644 --- a/tests/Internal/CancellationQueueTest.php +++ b/tests/Internal/CancellationQueueTest.php @@ -96,7 +96,7 @@ public function rethrowsExceptionsThrownFromCancel(): void $cancellationQueue(); } - private function getCancellableDeferred(): Deferred + private function getCancellableDeferred(): Deferred /** @phpstan-ignore-line */ { return new Deferred($this->expectCallableOnce()); } diff --git a/tests/Internal/FulfilledPromiseTest.php b/tests/Internal/FulfilledPromiseTest.php index 073b9d7a..390ffad0 100644 --- a/tests/Internal/FulfilledPromiseTest.php +++ b/tests/Internal/FulfilledPromiseTest.php @@ -9,6 +9,9 @@ use React\Promise\PromiseTest\PromiseSettledTestTrait; use React\Promise\TestCase; +/** + * @template T + */ class FulfilledPromiseTest extends TestCase { use PromiseSettledTestTrait, @@ -16,7 +19,7 @@ class FulfilledPromiseTest extends TestCase 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..e37c8612 100644 --- a/tests/Internal/RejectedPromiseTest.php +++ b/tests/Internal/RejectedPromiseTest.php @@ -5,10 +5,14 @@ use Exception; use LogicException; use React\Promise\PromiseAdapter\CallbackPromiseAdapter; +use React\Promise\PromiseInterface; use React\Promise\PromiseTest\PromiseRejectedTestTrait; use React\Promise\PromiseTest\PromiseSettledTestTrait; use React\Promise\TestCase; +/** + * @template T + */ class RejectedPromiseTest extends TestCase { use PromiseSettledTestTrait, @@ -16,7 +20,7 @@ class RejectedPromiseTest extends TestCase public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter { - /** @var ?RejectedPromise */ + /** @var ?RejectedPromise */ $promise = null; return new CallbackPromiseAdapter([ diff --git a/tests/PromiseAdapter/CallbackPromiseAdapter.php b/tests/PromiseAdapter/CallbackPromiseAdapter.php index 14a0acd4..aec72949 100644 --- a/tests/PromiseAdapter/CallbackPromiseAdapter.php +++ b/tests/PromiseAdapter/CallbackPromiseAdapter.php @@ -17,6 +17,9 @@ public function __construct(array $callbacks) $this->callbacks = $callbacks; } + /** + * @phpstan-ignore-next-line + */ public function promise(): PromiseInterface { return ($this->callbacks['promise'])(...func_get_args()); diff --git a/tests/PromiseAdapter/PromiseAdapterInterface.php b/tests/PromiseAdapter/PromiseAdapterInterface.php index 727fd514..0eaa2dcb 100644 --- a/tests/PromiseAdapter/PromiseAdapterInterface.php +++ b/tests/PromiseAdapter/PromiseAdapterInterface.php @@ -6,6 +6,9 @@ interface PromiseAdapterInterface { + /** + * @phpstan-ignore-next-line + */ public function promise(): PromiseInterface; public function resolve(): void; public function reject(): void; diff --git a/tests/PromiseTest/PromiseFulfilledTestTrait.php b/tests/PromiseTest/PromiseFulfilledTestTrait.php index d982214a..be5e7429 100644 --- a/tests/PromiseTest/PromiseFulfilledTestTrait.php +++ b/tests/PromiseTest/PromiseFulfilledTestTrait.php @@ -191,7 +191,7 @@ public function thenShouldContinueToExecuteCallbacksWhenPriorCallbackSuspendsFib $adapter->resolve(42); $fiber = new \Fiber(function () use ($adapter) { - $adapter->promise()->then(function (int $value) { + $adapter->promise()->then(function (int $value) { /** @phpstan-ignore-line */ \Fiber::suspend($value); }); }); diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index ad55ca28..12f6031f 100644 --- a/tests/PromiseTest/RejectTestTrait.php +++ b/tests/PromiseTest/RejectTestTrait.php @@ -73,7 +73,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) { $adapter->reject($exception3); return reject($value); diff --git a/types/Promises.php b/types/Promises.php new file mode 100644 index 00000000..e62c15c9 --- /dev/null +++ b/types/Promises.php @@ -0,0 +1,79 @@ +', resolve(true)); +assertType('React\Promise\PromiseInterface', resolve($stringOrInt())); +assertType('React\Promise\PromiseInterface', resolve(resolve(true))); + +/** + * chaining + */ +assertType('React\Promise\PromiseInterface', resolve(true)->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then()->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null)->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then($passThroughBoolFn)->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then($passThroughBoolFn, $passThroughThrowable)->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, $passThroughThrowable)->then($passThroughBoolFn)); +assertType('React\Promise\PromiseInterface', resolve(true)->then()->then(null, $passThroughThrowable)->then($passThroughBoolFn)); + +/** + * all + */ +assertType('React\Promise\PromiseInterface>', 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())])); +//assertType('React\Promise\PromiseInterface>', all([resolve(true), microtime(true)])); +//assertType('React\Promise\PromiseInterface>', all([true, resolve(time())])); + +/** + * any + */ +assertType('React\Promise\PromiseInterface', 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())])); +//assertType('React\Promise\PromiseInterface', any([resolve(true), microtime(true)])); +//assertType('React\Promise\PromiseInterface', any([true, resolve(time())])); + +/** + * race + */ +assertType('React\Promise\PromiseInterface', 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())])); +//assertType('React\Promise\PromiseInterface', race([resolve(true), microtime(true)])); +//assertType('React\Promise\PromiseInterface', race([true, resolve(time())])); + +/** + * direct class access (deprecated!!!) + */ +assertType('React\Promise\Internal\FulfilledPromise', new FulfilledPromise(true)); +//assertType('React\Promise\PromiseInterface', (new FulfilledPromise(true))->then($passThroughBoolFn));