Skip to content

Commit

Permalink
Merge pull request #246 from clue-labs/phpstan-v3
Browse files Browse the repository at this point in the history
[3.x] Add PHPStan to test environment with `max` level
  • Loading branch information
WyriHaximus committed Jun 21, 2023
2 parents 6019855 + c4e6145 commit d66fa66
Show file tree
Hide file tree
Showing 45 changed files with 448 additions and 386 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/.gitattributes export-ignore
/.github/ export-ignore
/.gitignore export-ignore
/phpstan.neon.dist export-ignore
/phpunit.xml.dist export-ignore
/phpunit.xml.legacy export-ignore
/tests/ export-ignore
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,25 @@ jobs:
if: ${{ matrix.php >= 7.3 }}
- run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy
if: ${{ matrix.php < 7.3 }}

PHPStan:
name: PHPStan (PHP ${{ matrix.php }})
runs-on: ubuntu-22.04
strategy:
matrix:
php:
- 8.2
- 8.1
- 8.0
- 7.4
- 7.3
- 7.2
- 7.1
steps:
- uses: actions/checkout@v3
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- run: composer install
- run: vendor/bin/phpstan
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ Table of Contents
* [Rejection forwarding](#rejection-forwarding)
* [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding)
5. [Install](#install)
6. [Credits](#credits)
7. [License](#license)
6. [Tests](#tests)
7. [Credits](#credits)
8. [License](#license)

Introduction
------------
Expand Down Expand Up @@ -586,6 +587,27 @@ PHP versions like this:
composer require "react/promise:^3@dev || ^2 || ^1"
```

## Tests

To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):

```bash
composer install
```

To run the test suite, go to the project root and run:

```bash
vendor/bin/phpunit
```

On top of this, we use PHPStan on max level to ensure type safety across the project:

```bash
vendor/bin/phpstan
```

Credits
-------

Expand Down
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,27 @@
"php": ">=7.1.0"
},
"require-dev": {
"phpstan/phpstan": "1.10.20 || 1.4.10",
"phpunit/phpunit": "^9.5 || ^7.5"
},
"autoload": {
"psr-4": {
"React\\Promise\\": "src/"
},
"files": ["src/functions_include.php"]
"files": [
"src/functions_include.php"
]
},
"autoload-dev": {
"psr-4": {
"React\\Promise\\": [
"tests/fixtures/",
"tests/"
]
}
},
"files": [
"tests/Fiber.php"
]
},
"keywords": [
"promise",
Expand Down
6 changes: 6 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
level: max

paths:
- src/
- tests/
8 changes: 8 additions & 0 deletions src/Deferred.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@

final class Deferred
{
/** @var Promise */
private $promise;

/** @var callable */
private $resolveCallback;

/** @var callable */
private $rejectCallback;

public function __construct(callable $canceller = null)
Expand All @@ -21,6 +26,9 @@ public function promise(): PromiseInterface
return $this->promise;
}

/**
* @param mixed $value
*/
public function resolve($value): void
{
($this->resolveCallback)($value);
Expand Down
4 changes: 3 additions & 1 deletion src/Exception/CompositeException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
*/
class CompositeException extends \Exception
{
/** @var \Throwable[] */
private $throwables;

public function __construct(array $throwables, $message = '', $code = 0, $previous = null)
/** @param \Throwable[] $throwables */
public function __construct(array $throwables, string $message = '', int $code = 0, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);

Expand Down
7 changes: 7 additions & 0 deletions src/Internal/CancellationQueue.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
*/
final class CancellationQueue
{
/** @var bool */
private $started = false;

/** @var object[] */
private $queue = [];

public function __invoke(): void
Expand All @@ -20,6 +23,9 @@ public function __invoke(): void
$this->drain();
}

/**
* @param mixed $cancellable
*/
public function enqueue($cancellable): void
{
if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) {
Expand All @@ -37,6 +43,7 @@ private function drain(): void
{
for ($i = \key($this->queue); isset($this->queue[$i]); $i++) {
$cancellable = $this->queue[$i];
assert(\method_exists($cancellable, 'cancel'));

$exception = null;

Expand Down
5 changes: 5 additions & 0 deletions src/Internal/FulfilledPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@
*/
final class FulfilledPromise implements PromiseInterface
{
/** @var mixed */
private $value;

/**
* @param mixed $value
* @throws \InvalidArgumentException
*/
public function __construct($value = null)
{
if ($value instanceof PromiseInterface) {
Expand Down
4 changes: 4 additions & 0 deletions src/Internal/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
*/
final class RejectedPromise implements PromiseInterface
{
/** @var \Throwable */
private $reason;

/**
* @param \Throwable $reason
*/
public function __construct(\Throwable $reason)
{
$this->reason = $reason;
Expand Down
9 changes: 8 additions & 1 deletion src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@

final class Promise implements PromiseInterface
{
/** @var ?callable */
private $canceller;

/** @var ?PromiseInterface */
private $result;

/** @var callable[] */
private $handlers = [];

/** @var int */
private $requiredCancelRequests = 0;

public function __construct(callable $resolver, callable $canceller = null)
Expand Down Expand Up @@ -46,6 +51,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
return new static(
$this->resolver($onFulfilled, $onRejected),
static function () use (&$parent) {
assert($parent instanceof self);
--$parent->requiredCancelRequests;

if ($parent->requiredCancelRequests <= 0) {
Expand Down Expand Up @@ -187,7 +193,7 @@ private function settle(PromiseInterface $result): void
}
}

private function unwrap($promise): PromiseInterface
private function unwrap(PromiseInterface $promise): PromiseInterface
{
while ($promise instanceof self && null !== $promise->result) {
$promise = $promise->result;
Expand All @@ -213,6 +219,7 @@ private function call(callable $cb): void
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
$ref = new \ReflectionMethod($callback, '__invoke');
} else {
assert($callback instanceof \Closure || \is_string($callback));
$ref = new \ReflectionFunction($callback);
}
$args = $ref->getNumberOfParameters();
Expand Down
19 changes: 12 additions & 7 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ function reject(\Throwable $reason): PromiseInterface
* will be an array containing the resolution values of each of the items in
* `$promisesOrValues`.
*
* @param iterable $promisesOrValues
* @param iterable<mixed> $promisesOrValues
* @return PromiseInterface
*/
function all(iterable $promisesOrValues): PromiseInterface
Expand All @@ -77,6 +77,7 @@ function all(iterable $promisesOrValues): PromiseInterface

return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
$toResolve = 0;
/** @var bool */
$continue = true;
$values = [];

Expand Down Expand Up @@ -118,7 +119,7 @@ function (\Throwable $reason) use (&$continue, $reject): void {
* The returned promise will become **infinitely pending** if `$promisesOrValues`
* contains 0 items.
*
* @param iterable $promisesOrValues
* @param iterable<mixed> $promisesOrValues
* @return PromiseInterface
*/
function race(iterable $promisesOrValues): PromiseInterface
Expand Down Expand Up @@ -153,7 +154,7 @@ 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
* @param iterable<mixed> $promisesOrValues
* @return PromiseInterface
*/
function any(iterable $promisesOrValues): PromiseInterface
Expand Down Expand Up @@ -215,6 +216,7 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
$callbackReflection = new \ReflectionMethod($callback, '__invoke');
} else {
assert($callback instanceof \Closure || \is_string($callback));
$callbackReflection = new \ReflectionFunction($callback);
}

Expand Down Expand Up @@ -256,14 +258,17 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool

if ($type instanceof \ReflectionIntersectionType) {
foreach ($type->getTypes() as $typeToMatch) {
if (!($matches = ($typeToMatch->isBuiltin() && \gettype($reason) === $typeToMatch->getName())
|| (new \ReflectionClass($typeToMatch->getName()))->isInstance($reason))) {
assert($typeToMatch instanceof \ReflectionNamedType);
$name = $typeToMatch->getName();
if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) {
break;
}
}
assert(isset($matches));
} else {
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
|| (new \ReflectionClass($type->getName()))->isInstance($reason);
assert($type instanceof \ReflectionNamedType);
$name = $type->getName();
$matches = !$type->isBuiltin() && $reason instanceof $name;
}

// If we look for a single match (union), we can return early on match
Expand Down
12 changes: 7 additions & 5 deletions tests/DeferredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class DeferredTest extends TestCase
{
use PromiseTest\FullTestTrait;

public function getPromiseTestAdapter(callable $canceller = null)
public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter
{
$d = new Deferred($canceller);

Expand All @@ -21,7 +21,7 @@ public function getPromiseTestAdapter(callable $canceller = null)
}

/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException(): void
{
gc_collect_cycles();
$deferred = new Deferred(function ($resolve, $reject) {
Expand All @@ -34,7 +34,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
}

/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException()
public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejectsWithException(): void
{
gc_collect_cycles();
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on
Expand All @@ -49,12 +49,14 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfParentCancellerRejects
}

/** @test */
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndExplicitlyRejectWithException()
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenceAndExplicitlyRejectWithException(): void
{
gc_collect_cycles();
gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on

$deferred = new Deferred(function () use (&$deferred) { });
$deferred = new Deferred(function () use (&$deferred) {
assert($deferred instanceof Deferred);
});
$deferred->reject(new \Exception('foo'));
unset($deferred);

Expand Down
27 changes: 27 additions & 0 deletions tests/Fiber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

if (!class_exists(Fiber::class)) {
/**
* Fiber stub to make PHPStan happy on PHP < 8.1
*
* @link https://www.php.net/manual/en/class.fiber.php
* @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/framework-x with permission
*/
class Fiber
{
public static function suspend(mixed $value): void
{
// NOOP
}

public function __construct(callable $callback)
{
assert(is_callable($callback));
}

public function start(): int
{
return 42;
}
}
}

0 comments on commit d66fa66

Please sign in to comment.