From f339647a7847b9caa5d1c8cc80724f70f0f66c17 Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 15 Mar 2021 11:55:46 +0100 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 --- .gitattributes | 1 + .github/workflows/ci.yml | 4 ++ phpstan.legacy.neon.dist | 9 ++++ src/Deferred.php | 12 +++++- src/Internal/FulfilledPromise.php | 18 ++++++-- src/Internal/RejectedPromise.php | 8 ++++ src/Promise.php | 20 +++++++-- src/PromiseInterface.php | 29 +++++++++---- src/functions.php | 36 ++++++++++------ tests/DeferredTest.php | 1 - tests/FunctionAnyTest.php | 1 + ...solveTestThenShouldNotReportUnhandled.phpt | 4 +- tests/Internal/CancellationQueueTest.php | 3 ++ tests/Internal/FulfilledPromiseTest.php | 5 ++- tests/Internal/RejectedPromiseTest.php | 5 ++- .../PromiseAdapter/CallbackPromiseAdapter.php | 3 ++ .../PromiseAdapterInterface.php | 3 ++ tests/PromiseTest.php | 3 -- .../PromiseTest/PromiseFulfilledTestTrait.php | 4 +- .../PromiseTest/PromiseRejectedTestTrait.php | 2 +- tests/PromiseTest/RejectTestTrait.php | 4 +- tests/PromiseTest/ResolveTestTrait.php | 2 +- tests/types/basic.php | 16 +++++++ tests/types/chaining.php | 25 +++++++++++ tests/types/changing_types.php | 28 +++++++++++++ tests/types/common.php | 42 +++++++++++++++++++ tests/types/deferred.php | 11 +++++ tests/types/deprecated.php | 19 +++++++++ tests/types/functions_all.php | 15 +++++++ tests/types/functions_any.php | 15 +++++++ tests/types/functions_race.php | 15 +++++++ tests/types/reject.php | 24 +++++++++++ tests/types/then.php | 15 +++++++ tests/types/void.php | 16 +++++++ 34 files changed, 375 insertions(+), 43 deletions(-) create mode 100644 phpstan.legacy.neon.dist create mode 100644 tests/types/basic.php create mode 100644 tests/types/chaining.php create mode 100644 tests/types/changing_types.php create mode 100644 tests/types/common.php create mode 100644 tests/types/deferred.php create mode 100644 tests/types/deprecated.php create mode 100644 tests/types/functions_all.php create mode 100644 tests/types/functions_any.php create mode 100644 tests/types/functions_race.php create mode 100644 tests/types/reject.php create mode 100644 tests/types/then.php create mode 100644 tests/types/void.php diff --git a/.gitattributes b/.gitattributes index 5d5606da..01e0208b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore +/phpstan.legacy.neon.dist export-ignore /phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43c9cb3f..9c2278b6 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 @@ -52,3 +53,6 @@ jobs: coverage: none - run: composer install - run: vendor/bin/phpstan + if: ${{ matrix.php >= 7.2 }} + - run: vendor/bin/phpstan --configuration="phpstan.legacy.neon.dist" + if: ${{ matrix.php < 7.2 }} diff --git a/phpstan.legacy.neon.dist b/phpstan.legacy.neon.dist new file mode 100644 index 00000000..66660f58 --- /dev/null +++ b/phpstan.legacy.neon.dist @@ -0,0 +1,9 @@ +parameters: + ignoreErrors: + - '#Template type T is declared as covariant, but occurs in contravariant position in parameter result of method React\\Promise\\Promise::settle\(\).#' + - '#Template type T is declared as covariant, but occurs in contravariant position in parameter promise of method React\\Promise\\Promise::unwrap\(\).#' + - '#PHPDoc tag @param has invalid value \(\?\(callable\(T\): \(Fulfilled\|void\)\) \$onFulfilled\): Unexpected token "\(", expected type at offset 1165#' + - '#Template type Fulfilled of method React\\Promise\\PromiseInterface::then\(\) is not referenced in a parameter.#' + +includes: + - phpstan.neon.dist 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..eb4176f9 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,11 @@ public function __construct($value = null) $this->value = $value; } + /** + * @template TFulfilled + * @param ?(callable((T is void ? null : T)): (PromiseInterface|TFulfilled)) $onFulfilled + * @return ($onFulfilled is null ? PromiseInterface : PromiseInterface) + */ 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..bb8b9a56 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 { @@ -37,6 +40,11 @@ public function __destruct() \error_log($message); } + /** + * @template TRejected + * @param ?(callable(\Throwable): (PromiseInterface|TRejected)) $onRejected + * @return ($onRejected is null ? PromiseInterface : PromiseInterface) + */ public function then(callable $onFulfilled = null, callable $onRejected = null): PromiseInterface { if (null === $onRejected) { diff --git a/src/Promise.php b/src/Promise.php index 819e414a..9282348a 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -4,12 +4,16 @@ use React\Promise\Internal\RejectedPromise; +/** + * @template-implements PromiseInterface + * @template-covariant T + */ final class Promise implements PromiseInterface { /** @var ?callable */ private $canceller; - /** @var ?PromiseInterface */ + /** @var ?PromiseInterface */ private $result; /** @var callable[] */ @@ -80,11 +84,11 @@ public function catch(callable $onRejected): PromiseInterface 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(static function () use ($value) { return $value; }); }, static function ($reason) use ($onFulfilledOrRejected) { - return resolve($onFulfilledOrRejected())->then(function () use ($reason) { + return resolve($onFulfilledOrRejected())->then(static function () use ($reason) { return new RejectedPromise($reason); }); }); @@ -175,6 +179,10 @@ private function reject(\Throwable $reason): void $this->settle(reject($reason)); } + /** + * Test out if null can be promise + * @param PromiseInterface|PromiseInterface $result + */ private function settle(PromiseInterface $result): void { $result = $this->unwrap($result); @@ -207,13 +215,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 diff --git a/src/PromiseInterface.php b/src/PromiseInterface.php index 47117072..02f18a6a 100644 --- a/src/PromiseInterface.php +++ b/src/PromiseInterface.php @@ -2,6 +2,9 @@ namespace React\Promise; +/** + * @template-covariant T + */ interface PromiseInterface { /** @@ -28,9 +31,18 @@ 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 ($onRejected is null ? + * ($onFulfilled is null ? PromiseInterface : ( + * T is never ? PromiseInterface : PromiseInterface + * )) : + * ($onFulfilled is null ? PromiseInterface : ( + * PromiseInterface<(T is never ? TRejected : TFulfilled|TRejected)> + * )) + * ) */ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): PromiseInterface; @@ -44,8 +56,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 +102,8 @@ public function catch(callable $onRejected): PromiseInterface; * ->finally('cleanup'); * ``` * - * @param callable $onFulfilledOrRejected - * @return PromiseInterface + * @param callable(): (mixed|void) $onFulfilledOrRejected + * @return PromiseInterface */ public function finally(callable $onFulfilledOrRejected): PromiseInterface; @@ -118,7 +129,7 @@ public function cancel(): void; * ``` * * @param callable $onRejected - * @return PromiseInterface + * @return PromiseInterface * @deprecated 3.0.0 Use catch() instead * @see self::catch() */ @@ -135,7 +146,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 c8107f8d..2fc60d54 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,6 +31,7 @@ function resolve($promiseOrValue): PromiseInterface $canceller = null; if (\method_exists($promiseOrValue, 'cancel')) { + /** @var callable $canceller */ $canceller = [$promiseOrValue, 'cancel']; } @@ -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 { @@ -86,7 +88,11 @@ function all(iterable $promisesOrValues): PromiseInterface $values[$i] = null; ++$toResolve; - resolve($promiseOrValue)->then( + /** + * @var PromiseInterface $p + */ + $p = resolve($promiseOrValue); + $p->then( function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { $values[$i] = $value; @@ -119,8 +125,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 iterable|T> $promisesOrValues + * @return PromiseInterface */ function race(iterable $promisesOrValues): PromiseInterface { @@ -154,8 +161,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 { @@ -170,7 +178,11 @@ function any(iterable $promisesOrValues): PromiseInterface $cancellationQueue->enqueue($promiseOrValue); ++$toReject; - resolve($promiseOrValue)->then( + /** + * @var PromiseInterface $p + */ + $p = resolve($promiseOrValue); + $p->then( function ($value) use ($resolve, &$continue): void { $continue = false; $resolve($value); diff --git a/tests/DeferredTest.php b/tests/DeferredTest.php index 186ed1d7..47209f8d 100644 --- a/tests/DeferredTest.php +++ b/tests/DeferredTest.php @@ -54,7 +54,6 @@ 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 */ $deferred = new Deferred(function () use (&$deferred) { assert($deferred instanceof Deferred); }); diff --git a/tests/FunctionAnyTest.php b/tests/FunctionAnyTest.php index 563f882e..09c4b34b 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/FunctionAnyTest.php @@ -52,6 +52,7 @@ public function shouldResolveWithAnInputValue(): void ->expects(self::once()) ->method('__invoke') ->with(self::identicalTo(1)); + assert(is_callable($mock)); any([1, 2, 3]) ->then($mock); diff --git a/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt b/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt index aabeae16..d00aca82 100644 --- a/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt +++ b/tests/FunctionResolveTestThenShouldNotReportUnhandled.phpt @@ -10,7 +10,9 @@ use function React\Promise\resolve; require __DIR__ . '/../vendor/autoload.php'; -resolve(42)->then('var_dump'); +/** @var callable $callable */ +$callable = 'var_dump'; +resolve(42)->then($callable); ?> --EXPECT-- 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..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..d1ac09d5 100644 --- a/tests/Internal/RejectedPromiseTest.php +++ b/tests/Internal/RejectedPromiseTest.php @@ -9,6 +9,9 @@ use React\Promise\PromiseTest\PromiseSettledTestTrait; use React\Promise\TestCase; +/** + * @template T + */ class RejectedPromiseTest extends TestCase { use PromiseSettledTestTrait, @@ -16,7 +19,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.php b/tests/PromiseTest.php index 06d89eb1..bc41e6a6 100644 --- a/tests/PromiseTest.php +++ b/tests/PromiseTest.php @@ -146,7 +146,6 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverThrowsExceptio public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReferenceThrowsException(): void { gc_collect_cycles(); - /** @var Promise $promise */ $promise = new Promise(function () {}, function () use (&$promise) { assert($promise instanceof Promise); throw new \Exception('foo'); @@ -165,7 +164,6 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceThrowsException(): void { gc_collect_cycles(); - /** @var Promise $promise */ $promise = new Promise(function () use (&$promise) { assert($promise instanceof Promise); throw new \Exception('foo'); @@ -186,7 +184,6 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndResolverThrowsException(): void { gc_collect_cycles(); - /** @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..a85eef4a 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); }); }); @@ -324,7 +324,7 @@ public function finallyShouldRejectWhenHandlerRejectsForFulfilledPromise(): void $adapter->resolve(1); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(function () use ($exception) { /** @phpstan-ignore-line */ return reject($exception); }) ->then(null, $mock); diff --git a/tests/PromiseTest/PromiseRejectedTestTrait.php b/tests/PromiseTest/PromiseRejectedTestTrait.php index 12287ce8..42e5d451 100644 --- a/tests/PromiseTest/PromiseRejectedTestTrait.php +++ b/tests/PromiseTest/PromiseRejectedTestTrait.php @@ -363,7 +363,7 @@ public function finallyShouldRejectWhenHandlerRejectsForRejectedPromise(): void $adapter->reject($exception1); $adapter->promise() - ->finally(function () use ($exception2) { + ->finally(function () use ($exception2) { /** @phpstan-ignore-line */ return reject($exception2); }) ->then(null, $mock); diff --git a/tests/PromiseTest/RejectTestTrait.php b/tests/PromiseTest/RejectTestTrait.php index ad55ca28..bb00e40f 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); @@ -206,7 +206,7 @@ public function finallyShouldRejectWhenHandlerRejectsForRejection(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(function () use ($exception) { /** @phpstan-ignore-line */ return reject($exception); }) ->then(null, $mock); diff --git a/tests/PromiseTest/ResolveTestTrait.php b/tests/PromiseTest/ResolveTestTrait.php index 357fdedc..00dab79e 100644 --- a/tests/PromiseTest/ResolveTestTrait.php +++ b/tests/PromiseTest/ResolveTestTrait.php @@ -266,7 +266,7 @@ public function finallyShouldRejectWhenHandlerRejectsForFulfillment(): void ->with($this->identicalTo($exception)); $adapter->promise() - ->finally(function () use ($exception) { + ->finally(function () use ($exception) { /** @phpstan-ignore-line */ return reject($exception); }) ->then(null, $mock); diff --git a/tests/types/basic.php b/tests/types/basic.php new file mode 100644 index 00000000..13b20bf1 --- /dev/null +++ b/tests/types/basic.php @@ -0,0 +1,16 @@ +', resolve(true)); +assertType('React\Promise\PromiseInterface', resolve(Types\stringOrInt())); +assertType('React\Promise\PromiseInterface', Types\stringOrIntPromise()); +assertType('React\Promise\PromiseInterface', resolve(resolve(true))); diff --git a/tests/types/chaining.php b/tests/types/chaining.php new file mode 100644 index 00000000..22a1acf5 --- /dev/null +++ b/tests/types/chaining.php @@ -0,0 +1,25 @@ +', resolve(true)->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then()->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null)->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then('React\Promise\Types\passThroughBoolFn')->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then('React\Promise\Types\passThroughBoolFn', 'React\Promise\Types\passThroughThrowable')->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, 'React\Promise\Types\passThroughThrowable')->then('React\Promise\Types\passThroughBoolFn')); +assertType('React\Promise\PromiseInterface', resolve(true)->then()->then(null, 'React\Promise\Types\passThroughThrowable')->then('React\Promise\Types\passThroughBoolFn')); diff --git a/tests/types/changing_types.php b/tests/types/changing_types.php new file mode 100644 index 00000000..59345889 --- /dev/null +++ b/tests/types/changing_types.php @@ -0,0 +1,28 @@ +', resolve(true)->then(function (bool $bool): int { + return time(); +})); +assertType('React\Promise\PromiseInterface', resolve(true)->then('React\Promise\Types\flipBoolToInt')); +assertType('React\Promise\PromiseInterface', resolve(true)->then(null, function (Throwable $throwable): int { + return time(); +})); +assertType('React\Promise\PromiseInterface<*NEVER*>', reject(new \Exception())->then(null, function (Throwable $throwable): int { + return time(); +})); diff --git a/tests/types/common.php b/tests/types/common.php new file mode 100644 index 00000000..2cea90bc --- /dev/null +++ b/tests/types/common.php @@ -0,0 +1,42 @@ + + */ +function passThroughThrowable(Throwable $t): PromiseInterface { + return reject($t); +}; + +/** + * @return int|string + */ +function stringOrInt() { + return time() % 2 ? 'string' : time(); +}; + +/** + * @return PromiseInterface + */ +function stringOrIntPromise(): PromiseInterface { + return resolve(time() % 2 ? 'string' : time()); +}; + +/** + * @return PromiseInterface + */ +function flipBoolToInt(bool $bool): PromiseInterface { + return resolve(time()); +}; + diff --git a/tests/types/deferred.php b/tests/types/deferred.php new file mode 100644 index 00000000..de0c3260 --- /dev/null +++ b/tests/types/deferred.php @@ -0,0 +1,11 @@ +', $deferredA->promise()); + +$deferredB = new Deferred(); +$deferredB->resolve(42); +//assertType('React\Promise\PromiseInterface', $deferredB->promise()); diff --git a/tests/types/deprecated.php b/tests/types/deprecated.php new file mode 100644 index 00000000..3f3e68e7 --- /dev/null +++ b/tests/types/deprecated.php @@ -0,0 +1,19 @@ +', new FulfilledPromise(true)); +assertType('React\Promise\PromiseInterface', (new FulfilledPromise(true))->then('React\Promise\Types\passThroughBoolFn')); + +/** + * reject + */ +assertType('React\Promise\PromiseInterface<*NEVER*>', new RejectedPromise(new RuntimeException())); diff --git a/tests/types/functions_all.php b/tests/types/functions_all.php new file mode 100644 index 00000000..44b643e0 --- /dev/null +++ b/tests/types/functions_all.php @@ -0,0 +1,15 @@ +>', 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/functions_any.php b/tests/types/functions_any.php new file mode 100644 index 00000000..78659dfe --- /dev/null +++ b/tests/types/functions_any.php @@ -0,0 +1,15 @@ +', 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/functions_race.php b/tests/types/functions_race.php new file mode 100644 index 00000000..e3e12af0 --- /dev/null +++ b/tests/types/functions_race.php @@ -0,0 +1,15 @@ +', 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..cc34af5f --- /dev/null +++ b/tests/types/reject.php @@ -0,0 +1,24 @@ +', 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())->catch(function (): int { + return 42; +})); + +assertType('React\Promise\PromiseInterface', reject(new RuntimeException())->then(function (): bool { + return true; +}, function (): int { + return 42; +})); diff --git a/tests/types/then.php b/tests/types/then.php new file mode 100644 index 00000000..041932c0 --- /dev/null +++ b/tests/types/then.php @@ -0,0 +1,15 @@ +', resolve(true)->then(static function ($v) use (&$value) { + $value = $v; +})); +// This passes on PHP 7.2+ but can't be ignored for PHP 7.1 +//assertType('bool|null', $value); diff --git a/tests/types/void.php b/tests/types/void.php new file mode 100644 index 00000000..011a7180 --- /dev/null +++ b/tests/types/void.php @@ -0,0 +1,16 @@ +', resolve(true)->then(function (bool $bool): void { + +})); +assertType('React\Promise\PromiseInterface', resolve(false)->then(function (bool $bool): void { + +})->then(function (null $value) { + +}));