Skip to content
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

[3.x] Add PHPStan to test environment with max level #246

Merged
merged 2 commits into from
Jun 21, 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
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;
}
}
}
Loading