Skip to content

Commit

Permalink
Merge pull request #253 from clue-labs/callable-types
Browse files Browse the repository at this point in the history
Describe all callable arguments with types for `Promise` and `Deferred`
  • Loading branch information
WyriHaximus committed Sep 11, 2023
2 parents baf9ab5 + de40371 commit 5cd1458
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 19 deletions.
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -318,7 +318,7 @@ $promise = new React\Promise\Promise($resolver, $canceller);
```

The promise constructor receives a resolver function and an optional canceller
function which both will be called with 3 arguments:
function which both will be called with two arguments:

* `$resolve($value)` - Primary function that seals the fate of the
returned promise. Accepts either a non-promise value, or another promise.
Expand Down
7 changes: 5 additions & 2 deletions src/Deferred.php
Expand Up @@ -12,12 +12,15 @@ final class Deferred
*/
private $promise;

/** @var callable */
/** @var callable(T):void */
private $resolveCallback;

/** @var callable */
/** @var callable(\Throwable):void */
private $rejectCallback;

/**
* @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller
*/
public function __construct(callable $canceller = null)
{
$this->promise = new Promise(function ($resolve, $reject): void {
Expand Down
31 changes: 19 additions & 12 deletions src/Promise.php
Expand Up @@ -10,13 +10,13 @@
*/
final class Promise implements PromiseInterface
{
/** @var ?callable */
/** @var (callable(callable(T):void,callable(\Throwable):void):void)|null */
private $canceller;

/** @var ?PromiseInterface<T> */
private $result;

/** @var callable[] */
/** @var list<callable(PromiseInterface<T>):void> */
private $handlers = [];

/** @var int */
Expand All @@ -25,6 +25,10 @@ final class Promise implements PromiseInterface
/** @var bool */
private $cancelled = false;

/**
* @param callable(callable(T):void,callable(\Throwable):void):void $resolver
* @param (callable(callable(T):void,callable(\Throwable):void):void)|null $canceller
*/
public function __construct(callable $resolver, callable $canceller = null)
{
$this->canceller = $canceller;
Expand Down Expand Up @@ -57,7 +61,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):

return new static(
$this->resolver($onFulfilled, $onRejected),
static function () use (&$parent) {
static function () use (&$parent): void {
assert($parent instanceof self);
--$parent->requiredCancelRequests;

Expand All @@ -78,7 +82,7 @@ static function () use (&$parent) {
*/
public function catch(callable $onRejected): PromiseInterface
{
return $this->then(null, static function ($reason) use ($onRejected) {
return $this->then(null, static function (\Throwable $reason) use ($onRejected) {
if (!_checkTypehint($onRejected, $reason)) {
return new RejectedPromise($reason);
}
Expand All @@ -92,12 +96,12 @@ public function catch(callable $onRejected): PromiseInterface

public function finally(callable $onFulfilledOrRejected): PromiseInterface
{
return $this->then(static function ($value) use ($onFulfilledOrRejected) {
return $this->then(static function ($value) use ($onFulfilledOrRejected): PromiseInterface {
return resolve($onFulfilledOrRejected())->then(function () use ($value) {
return $value;
});
}, static function ($reason) use ($onFulfilledOrRejected) {
return resolve($onFulfilledOrRejected())->then(function () use ($reason) {
}, static function (\Throwable $reason) use ($onFulfilledOrRejected): PromiseInterface {
return resolve($onFulfilledOrRejected())->then(function () use ($reason): RejectedPromise {
return new RejectedPromise($reason);
});
});
Expand Down Expand Up @@ -164,12 +168,12 @@ public function always(callable $onFulfilledOrRejected): PromiseInterface

private function resolver(callable $onFulfilled = null, callable $onRejected = null): callable
{
return function ($resolve, $reject) use ($onFulfilled, $onRejected) {
$this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject) {
return function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected): void {
$this->handlers[] = static function (PromiseInterface $promise) use ($onFulfilled, $onRejected, $resolve, $reject): void {
$promise = $promise->then($onFulfilled, $onRejected);

if ($promise instanceof self && $promise->result === null) {
$promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject) {
$promise->handlers[] = static function (PromiseInterface $promise) use ($resolve, $reject): void {
$promise->then($resolve, $reject);
};
} else {
Expand Down Expand Up @@ -237,6 +241,9 @@ private function unwrap(PromiseInterface $promise): PromiseInterface
return $promise;
}

/**
* @param callable(callable(mixed):void,callable(\Throwable):void):void $cb
*/
private function call(callable $cb): void
{
// Explicitly overwrite argument with null value. This ensure that this
Expand Down Expand Up @@ -274,13 +281,13 @@ private function call(callable $cb): void
$target =& $this;

$callback(
static function ($value) use (&$target) {
static function ($value) use (&$target): void {
if ($target !== null) {
$target->settle(resolve($value));
$target = null;
}
},
static function (\Throwable $reason) use (&$target) {
static function (\Throwable $reason) use (&$target): void {
if ($target !== null) {
$target->reject($reason);
$target = null;
Expand Down
10 changes: 7 additions & 3 deletions src/functions.php
Expand Up @@ -35,7 +35,8 @@ function resolve($promiseOrValue): PromiseInterface
assert(\is_callable($canceller));
}

return new Promise(function ($resolve, $reject) use ($promiseOrValue): void {
/** @var Promise<T> */
return new Promise(function (callable $resolve, callable $reject) use ($promiseOrValue): void {
$promiseOrValue->then($resolve, $reject);
}, $canceller);
}
Expand Down Expand Up @@ -77,7 +78,8 @@ function all(iterable $promisesOrValues): PromiseInterface
{
$cancellationQueue = new Internal\CancellationQueue();

return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
/** @var Promise<array<T>> */
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
$toResolve = 0;
/** @var bool */
$continue = true;
Expand Down Expand Up @@ -129,6 +131,7 @@ function race(iterable $promisesOrValues): PromiseInterface
{
$cancellationQueue = new Internal\CancellationQueue();

/** @var Promise<T> */
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
$continue = true;

Expand Down Expand Up @@ -165,7 +168,8 @@ function any(iterable $promisesOrValues): PromiseInterface
{
$cancellationQueue = new Internal\CancellationQueue();

return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
/** @var Promise<T> */
return new Promise(function (callable $resolve, callable $reject) use ($promisesOrValues, $cancellationQueue): void {
$toReject = 0;
$continue = true;
$reasons = [];
Expand Down
1 change: 1 addition & 0 deletions tests/Internal/CancellationQueueTest.php
Expand Up @@ -101,6 +101,7 @@ public function rethrowsExceptionsThrownFromCancel(): void
*/
private function getCancellableDeferred(): Deferred
{
/** @var Deferred<never> */
return new Deferred($this->expectCallableOnce());
}
}
5 changes: 4 additions & 1 deletion tests/PromiseTest.php
Expand Up @@ -19,11 +19,14 @@ public function getPromiseTestAdapter(callable $canceller = null): CallbackPromi
{
$resolveCallback = $rejectCallback = null;

$promise = new Promise(function ($resolve, $reject) use (&$resolveCallback, &$rejectCallback) {
$promise = new Promise(function (callable $resolve, callable $reject) use (&$resolveCallback, &$rejectCallback): void {
$resolveCallback = $resolve;
$rejectCallback = $reject;
}, $canceller);

assert(is_callable($resolveCallback));
assert(is_callable($rejectCallback));

return new CallbackPromiseAdapter([
'promise' => function () use ($promise) {
return $promise;
Expand Down
37 changes: 37 additions & 0 deletions tests/types/deferred.php
Expand Up @@ -10,3 +10,40 @@
$deferredB = new Deferred();
$deferredB->resolve(42);
assertType('React\Promise\PromiseInterface<int>', $deferredB->promise());

// $deferred = new Deferred();
// $deferred->resolve(42);
// assertType('React\Promise\Deferred<int>', $deferred);

// $deferred = new Deferred();
// $deferred->resolve(true);
// $deferred->resolve('ignored');
// assertType('React\Promise\Deferred<bool>', $deferred);

// $deferred = new Deferred();
// $deferred->reject(new \RuntimeException());
// assertType('React\Promise\Deferred<never>', $deferred);

// invalid number of arguments passed to $canceller
/** @phpstan-ignore-next-line */
$deferred = new Deferred(function ($a, $b, $c) { });
assertType('React\Promise\Deferred<mixed>', $deferred);

// invalid types for arguments of $canceller
/** @phpstan-ignore-next-line */
$deferred = new Deferred(function (int $a, string $b) { });
assertType('React\Promise\Deferred<mixed>', $deferred);

// invalid number of arguments passed to $resolve
$deferred = new Deferred(function (callable $resolve) {
/** @phpstan-ignore-next-line */
$resolve();
});
assertType('React\Promise\Deferred<mixed>', $deferred);

// invalid type passed to $reject
$deferred = new Deferred(function (callable $resolve, callable $reject) {
/** @phpstan-ignore-next-line */
$reject(2);
});
assertType('React\Promise\Deferred<mixed>', $deferred);
91 changes: 91 additions & 0 deletions tests/types/promise.php
@@ -0,0 +1,91 @@
<?php

use React\Promise\Promise;
use function PHPStan\Testing\assertType;

// $promise = new Promise(function (): void { });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// $promise = new Promise(function (callable $resolve): void {
// $resolve(42);
// });
// assertType('React\Promise\PromiseInterface<int>', $promise);

// $promise = new Promise(function (callable $resolve): void {
// $resolve(true);
// $resolve('ignored');
// });
// assertType('React\Promise\PromiseInterface<bool>', $promise);

// $promise = new Promise(function (callable $resolve, callable $reject): void {
// $reject(new \RuntimeException());
// });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// $promise = new Promise(function (): never {
// throw new \RuntimeException();
// });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments for $resolver
/** @phpstan-ignore-next-line */
$promise = new Promise(function ($a, $b, $c) { });
assert($promise instanceof Promise);
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid types for arguments of $resolver
/** @phpstan-ignore-next-line */
$promise = new Promise(function (int $a, string $b) { });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments passed to $resolve
$promise = new Promise(function (callable $resolve) {
/** @phpstan-ignore-next-line */
$resolve();
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments passed to $reject
$promise = new Promise(function (callable $resolve, callable $reject) {
/** @phpstan-ignore-next-line */
$reject();
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid type passed to $reject
$promise = new Promise(function (callable $resolve, callable $reject) {
/** @phpstan-ignore-next-line */
$reject(2);
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments for $canceller
/** @phpstan-ignore-next-line */
$promise = new Promise(function () { }, function ($a, $b, $c) { });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid types for arguments of $canceller
/** @phpstan-ignore-next-line */
$promise = new Promise(function () { }, function (int $a, string $b) { });
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments passed to $resolve
$promise = new Promise(function () { }, function (callable $resolve) {
/** @phpstan-ignore-next-line */
$resolve();
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid number of arguments passed to $reject
$promise = new Promise(function () { }, function (callable $resolve, callable $reject) {
/** @phpstan-ignore-next-line */
$reject();
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

// invalid type passed to $reject
$promise = new Promise(function() { }, function (callable $resolve, callable $reject) {
/** @phpstan-ignore-next-line */
$reject(2);
});
// assertType('React\Promise\PromiseInterface<never>', $promise);

0 comments on commit 5cd1458

Please sign in to comment.