diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2c89d7a..98a9fef2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,3 +50,16 @@ jobs: uses: docker://hhvm/hhvm:3.30-lts-latest with: args: hhvm vendor/bin/phpunit + + static-analysis: + name: PHPStan + runs-on: ubuntu-20.04 + continue-on-error: true + steps: + - uses: actions/checkout@v3 + - uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + - run: composer require phpstan/phpstan + - name: Execute type checking + run: vendor/bin/phpstan --configuration="phpstan.types.neon.dist" diff --git a/composer.json b/composer.json index 2a48ed1d..ee9befd7 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ } ], "require": { - "php": ">=5.4.0" + "php": ">=5.4.0", + "phpstan/phpstan": "^1.9" }, "require-dev": { "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.36" diff --git a/phpstan.types.neon.dist b/phpstan.types.neon.dist new file mode 100644 index 00000000..dc0455dd --- /dev/null +++ b/phpstan.types.neon.dist @@ -0,0 +1,4 @@ +parameters: + paths: + - types + level: max \ No newline at end of file diff --git a/src/ExtendedPromiseInterface.php b/src/ExtendedPromiseInterface.php index 13b63691..9fc704bd 100644 --- a/src/ExtendedPromiseInterface.php +++ b/src/ExtendedPromiseInterface.php @@ -78,8 +78,9 @@ public function otherwise(callable $onRejected); * ->always('cleanup'); * ``` * - * @param callable $onFulfilledOrRejected - * @return ExtendedPromiseInterface + * @template TReturn of mixed + * @param callable(T): TReturn $onFulfilledOrRejected + * @return (TReturn is ExtendedPromiseInterface ? TReturn : ExtendedPromiseInterface) */ public function always(callable $onFulfilledOrRejected); diff --git a/src/FulfilledPromise.php b/src/FulfilledPromise.php index 14727527..3c17244c 100644 --- a/src/FulfilledPromise.php +++ b/src/FulfilledPromise.php @@ -4,11 +4,19 @@ /** * @deprecated 2.8.0 External usage of FulfilledPromise is deprecated, use `resolve()` instead. + * @template-implements PromiseInterface + * @template-covariant T */ class FulfilledPromise implements ExtendedPromiseInterface, CancellablePromiseInterface { + /** + * @var T + */ private $value; + /** + * @param T $value + */ public function __construct($value = null) { if ($value instanceof PromiseInterface) { @@ -18,6 +26,11 @@ public function __construct($value = null) $this->value = $value; } + /** + * @template TFulfilled as PromiseInterface|T + * @param (callable(T): TFulfilled)|null $onFulfilled + * @return ($onFulfilled is null ? $this : (TFulfilled is PromiseInterface ? TFulfilled : PromiseInterface)) + */ public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null === $onFulfilled) { diff --git a/src/Promise.php b/src/Promise.php index 33759e6f..0096b285 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -2,9 +2,17 @@ namespace React\Promise; +/** + * @template-implements PromiseInterface + * @template-covariant T + */ class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface { private $canceller; + + /** + * @var PromiseInterface + */ private $result; private $handlers = []; @@ -25,6 +33,11 @@ public function __construct(callable $resolver, callable $canceller = null) $this->call($cb); } + /** + * @template TFulfilled as PromiseInterface|T + * @param (callable(T): TFulfilled)|null $onFulfilled + * @return ($onFulfilled is null ? $this : (TFulfilled is PromiseInterface ? TFulfilled : PromiseInterface)) + */ public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null) { if (null !== $this->result) { diff --git a/src/PromiseInterface.php b/src/PromiseInterface.php index edcb0077..821b0283 100644 --- a/src/PromiseInterface.php +++ b/src/PromiseInterface.php @@ -2,6 +2,9 @@ namespace React\Promise; +/** + * @template-covariant T + */ interface PromiseInterface { /** @@ -32,10 +35,9 @@ interface PromiseInterface * than once. * 3. `$onProgress` (deprecated) may be called multiple times. * - * @param callable|null $onFulfilled - * @param callable|null $onRejected - * @param callable|null $onProgress This argument is deprecated and should not be used anymore. - * @return PromiseInterface + * @template TFulfilled as PromiseInterface|T + * @param (callable(T): TFulfilled)|null $onFulfilled + * @return ($onFulfilled is null ? $this : (TFulfilled is PromiseInterface ? TFulfilled : PromiseInterface)) */ public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null); } diff --git a/src/functions.php b/src/functions.php index 2177dc23..b36364ca 100644 --- a/src/functions.php +++ b/src/functions.php @@ -13,8 +13,10 @@ * * If `$promiseOrValue` is a promise, it will be returned as is. * - * @param mixed $promiseOrValue - * @return PromiseInterface + * @template-covariant T + * @template TFulfilled as PromiseInterface|T + * @param TFulfilled $promiseOrValue + * @return (TFulfilled is PromiseInterface ? TFulfilled : PromiseInterface) */ function resolve($promiseOrValue = null) { @@ -52,8 +54,10 @@ function resolve($promiseOrValue = null) * throwing an exception. For example, it allows you to propagate a rejection with * the value of another promise. * - * @param mixed $promiseOrValue - * @return PromiseInterface + * @template T is null + * @template R + * @param R $promiseOrValue + * @return PromiseInterface */ function reject($promiseOrValue = null) { @@ -72,8 +76,9 @@ function reject($promiseOrValue = null) * will be an array containing the resolution values of each of the items in * `$promisesOrValues`. * - * @param array $promisesOrValues - * @return PromiseInterface + * @template T + * @param array|T> $promisesOrValues + * @return PromiseInterface> */ function all($promisesOrValues) { @@ -89,8 +94,9 @@ function all($promisesOrValues) * The returned promise will become **infinitely pending** if `$promisesOrValues` * contains 0 items. * - * @param array $promisesOrValues - * @return PromiseInterface + * @template T + * @param array|T> $promisesOrValues + * @return PromiseInterface */ function race($promisesOrValues) { @@ -126,8 +132,9 @@ function race($promisesOrValues) * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains 0 items. * - * @param array $promisesOrValues - * @return PromiseInterface + * @template T + * @param array|T> $promisesOrValues + * @return PromiseInterface */ function any($promisesOrValues) { @@ -151,9 +158,10 @@ function any($promisesOrValues) * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains less items than `$howMany`. * - * @param array $promisesOrValues + * @template T + * @param array|T> $promisesOrValues * @param int $howMany - * @return PromiseInterface + * @return PromiseInterface> */ function some($promisesOrValues, $howMany) { @@ -228,9 +236,11 @@ function some($promisesOrValues, $howMany) * The map function receives each item as argument, where item is a fully resolved * value of a promise or value in `$promisesOrValues`. * - * @param array $promisesOrValues - * @param callable $mapFunc - * @return PromiseInterface + * @template-covariant T + * @template TFulfilled as PromiseInterface|T + * @param array|T> $promisesOrValues + * @param callable(T): TFulfilled $mapFunc + * @return PromiseInterface> */ function map($promisesOrValues, callable $mapFunc) { @@ -276,10 +286,11 @@ function ($mapped) use ($i, &$values, &$toResolve, $resolve) { * promise, *and* `$initialValue` may be a promise or a value for the starting * value. * - * @param array $promisesOrValues - * @param callable $reduceFunc + * @template T + * @param array|T> $promisesOrValues + * @param callable(T): bool $reduceFunc * @param mixed $initialValue - * @return PromiseInterface + * @return PromiseInterface> */ function reduce($promisesOrValues, callable $reduceFunc, $initialValue = null) { diff --git a/types/Promises.php b/types/Promises.php new file mode 100644 index 00000000..d137be6e --- /dev/null +++ b/types/Promises.php @@ -0,0 +1,75 @@ + $bool; +$passThroughThrowable = static function (Throwable $t): PromiseInterface { + return reject($t); +}; +$stringOrInt = function (): int|string { + return time() % 2 ? 'string' : time(); +}; +$tosseable = new Exception('Oops I did it again!'); + +/** + * basic + */ +assertType('React\Promise\PromiseInterface', 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), hrtime()])); +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), hrtime()])); +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), hrtime()])); +assertType('React\Promise\PromiseInterface', race([true, resolve(time())])); + +/** + * direct class access (deprecated!!!) + */ +assertType('React\Promise\FulfilledPromise', new FulfilledPromise(true)); +assertType('React\Promise\PromiseInterface', (new FulfilledPromise(true))->then($passThroughBoolFn));