Skip to content

Describe all callable arguments with types for Promise and Deferred #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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);