Skip to content

Commit d66fa66

Browse files
authored
Merge pull request #246 from clue-labs/phpstan-v3
[3.x] Add PHPStan to test environment with `max` level
2 parents 6019855 + c4e6145 commit d66fa66

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+448
-386
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/.gitattributes export-ignore
22
/.github/ export-ignore
33
/.gitignore export-ignore
4+
/phpstan.neon.dist export-ignore
45
/phpunit.xml.dist export-ignore
56
/phpunit.xml.legacy export-ignore
67
/tests/ export-ignore

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,25 @@ jobs:
3030
if: ${{ matrix.php >= 7.3 }}
3131
- run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy
3232
if: ${{ matrix.php < 7.3 }}
33+
34+
PHPStan:
35+
name: PHPStan (PHP ${{ matrix.php }})
36+
runs-on: ubuntu-22.04
37+
strategy:
38+
matrix:
39+
php:
40+
- 8.2
41+
- 8.1
42+
- 8.0
43+
- 7.4
44+
- 7.3
45+
- 7.2
46+
- 7.1
47+
steps:
48+
- uses: actions/checkout@v3
49+
- uses: shivammathur/setup-php@v2
50+
with:
51+
php-version: ${{ matrix.php }}
52+
coverage: none
53+
- run: composer install
54+
- run: vendor/bin/phpstan

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ Table of Contents
4848
* [Rejection forwarding](#rejection-forwarding)
4949
* [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding)
5050
5. [Install](#install)
51-
6. [Credits](#credits)
52-
7. [License](#license)
51+
6. [Tests](#tests)
52+
7. [Credits](#credits)
53+
8. [License](#license)
5354

5455
Introduction
5556
------------
@@ -586,6 +587,27 @@ PHP versions like this:
586587
composer require "react/promise:^3@dev || ^2 || ^1"
587588
```
588589

590+
## Tests
591+
592+
To run the test suite, you first need to clone this repo and then install all
593+
dependencies [through Composer](https://getcomposer.org/):
594+
595+
```bash
596+
composer install
597+
```
598+
599+
To run the test suite, go to the project root and run:
600+
601+
```bash
602+
vendor/bin/phpunit
603+
```
604+
605+
On top of this, we use PHPStan on max level to ensure type safety across the project:
606+
607+
```bash
608+
vendor/bin/phpstan
609+
```
610+
589611
Credits
590612
-------
591613

composer.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,27 @@
2828
"php": ">=7.1.0"
2929
},
3030
"require-dev": {
31+
"phpstan/phpstan": "1.10.20 || 1.4.10",
3132
"phpunit/phpunit": "^9.5 || ^7.5"
3233
},
3334
"autoload": {
3435
"psr-4": {
3536
"React\\Promise\\": "src/"
3637
},
37-
"files": ["src/functions_include.php"]
38+
"files": [
39+
"src/functions_include.php"
40+
]
3841
},
3942
"autoload-dev": {
4043
"psr-4": {
4144
"React\\Promise\\": [
4245
"tests/fixtures/",
4346
"tests/"
4447
]
45-
}
48+
},
49+
"files": [
50+
"tests/Fiber.php"
51+
]
4652
},
4753
"keywords": [
4854
"promise",

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
parameters:
2+
level: max
3+
4+
paths:
5+
- src/
6+
- tests/

src/Deferred.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
final class Deferred
66
{
7+
/** @var Promise */
78
private $promise;
9+
10+
/** @var callable */
811
private $resolveCallback;
12+
13+
/** @var callable */
914
private $rejectCallback;
1015

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

29+
/**
30+
* @param mixed $value
31+
*/
2432
public function resolve($value): void
2533
{
2634
($this->resolveCallback)($value);

src/Exception/CompositeException.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111
*/
1212
class CompositeException extends \Exception
1313
{
14+
/** @var \Throwable[] */
1415
private $throwables;
1516

16-
public function __construct(array $throwables, $message = '', $code = 0, $previous = null)
17+
/** @param \Throwable[] $throwables */
18+
public function __construct(array $throwables, string $message = '', int $code = 0, ?\Throwable $previous = null)
1719
{
1820
parent::__construct($message, $code, $previous);
1921

src/Internal/CancellationQueue.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88
final class CancellationQueue
99
{
10+
/** @var bool */
1011
private $started = false;
12+
13+
/** @var object[] */
1114
private $queue = [];
1215

1316
public function __invoke(): void
@@ -20,6 +23,9 @@ public function __invoke(): void
2023
$this->drain();
2124
}
2225

26+
/**
27+
* @param mixed $cancellable
28+
*/
2329
public function enqueue($cancellable): void
2430
{
2531
if (!\is_object($cancellable) || !\method_exists($cancellable, 'then') || !\method_exists($cancellable, 'cancel')) {
@@ -37,6 +43,7 @@ private function drain(): void
3743
{
3844
for ($i = \key($this->queue); isset($this->queue[$i]); $i++) {
3945
$cancellable = $this->queue[$i];
46+
assert(\method_exists($cancellable, 'cancel'));
4047

4148
$exception = null;
4249

src/Internal/FulfilledPromise.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@
1010
*/
1111
final class FulfilledPromise implements PromiseInterface
1212
{
13+
/** @var mixed */
1314
private $value;
1415

16+
/**
17+
* @param mixed $value
18+
* @throws \InvalidArgumentException
19+
*/
1520
public function __construct($value = null)
1621
{
1722
if ($value instanceof PromiseInterface) {

src/Internal/RejectedPromise.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
*/
1212
final class RejectedPromise implements PromiseInterface
1313
{
14+
/** @var \Throwable */
1415
private $reason;
1516

17+
/**
18+
* @param \Throwable $reason
19+
*/
1620
public function __construct(\Throwable $reason)
1721
{
1822
$this->reason = $reason;

src/Promise.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@
66

77
final class Promise implements PromiseInterface
88
{
9+
/** @var ?callable */
910
private $canceller;
11+
12+
/** @var ?PromiseInterface */
1013
private $result;
1114

15+
/** @var callable[] */
1216
private $handlers = [];
1317

18+
/** @var int */
1419
private $requiredCancelRequests = 0;
1520

1621
public function __construct(callable $resolver, callable $canceller = null)
@@ -46,6 +51,7 @@ public function then(callable $onFulfilled = null, callable $onRejected = null):
4651
return new static(
4752
$this->resolver($onFulfilled, $onRejected),
4853
static function () use (&$parent) {
54+
assert($parent instanceof self);
4955
--$parent->requiredCancelRequests;
5056

5157
if ($parent->requiredCancelRequests <= 0) {
@@ -187,7 +193,7 @@ private function settle(PromiseInterface $result): void
187193
}
188194
}
189195

190-
private function unwrap($promise): PromiseInterface
196+
private function unwrap(PromiseInterface $promise): PromiseInterface
191197
{
192198
while ($promise instanceof self && null !== $promise->result) {
193199
$promise = $promise->result;
@@ -213,6 +219,7 @@ private function call(callable $cb): void
213219
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
214220
$ref = new \ReflectionMethod($callback, '__invoke');
215221
} else {
222+
assert($callback instanceof \Closure || \is_string($callback));
216223
$ref = new \ReflectionFunction($callback);
217224
}
218225
$args = $ref->getNumberOfParameters();

src/functions.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ function reject(\Throwable $reason): PromiseInterface
6868
* will be an array containing the resolution values of each of the items in
6969
* `$promisesOrValues`.
7070
*
71-
* @param iterable $promisesOrValues
71+
* @param iterable<mixed> $promisesOrValues
7272
* @return PromiseInterface
7373
*/
7474
function all(iterable $promisesOrValues): PromiseInterface
@@ -77,6 +77,7 @@ function all(iterable $promisesOrValues): PromiseInterface
7777

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

@@ -118,7 +119,7 @@ function (\Throwable $reason) use (&$continue, $reject): void {
118119
* The returned promise will become **infinitely pending** if `$promisesOrValues`
119120
* contains 0 items.
120121
*
121-
* @param iterable $promisesOrValues
122+
* @param iterable<mixed> $promisesOrValues
122123
* @return PromiseInterface
123124
*/
124125
function race(iterable $promisesOrValues): PromiseInterface
@@ -153,7 +154,7 @@ function race(iterable $promisesOrValues): PromiseInterface
153154
* The returned promise will also reject with a `React\Promise\Exception\LengthException`
154155
* if `$promisesOrValues` contains 0 items.
155156
*
156-
* @param iterable $promisesOrValues
157+
* @param iterable<mixed> $promisesOrValues
157158
* @return PromiseInterface
158159
*/
159160
function any(iterable $promisesOrValues): PromiseInterface
@@ -215,6 +216,7 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool
215216
} elseif (\is_object($callback) && !$callback instanceof \Closure) {
216217
$callbackReflection = new \ReflectionMethod($callback, '__invoke');
217218
} else {
219+
assert($callback instanceof \Closure || \is_string($callback));
218220
$callbackReflection = new \ReflectionFunction($callback);
219221
}
220222

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

257259
if ($type instanceof \ReflectionIntersectionType) {
258260
foreach ($type->getTypes() as $typeToMatch) {
259-
if (!($matches = ($typeToMatch->isBuiltin() && \gettype($reason) === $typeToMatch->getName())
260-
|| (new \ReflectionClass($typeToMatch->getName()))->isInstance($reason))) {
261+
assert($typeToMatch instanceof \ReflectionNamedType);
262+
$name = $typeToMatch->getName();
263+
if (!($matches = (!$typeToMatch->isBuiltin() && $reason instanceof $name))) {
261264
break;
262265
}
263266
}
267+
assert(isset($matches));
264268
} else {
265-
$matches = ($type->isBuiltin() && \gettype($reason) === $type->getName())
266-
|| (new \ReflectionClass($type->getName()))->isInstance($reason);
269+
assert($type instanceof \ReflectionNamedType);
270+
$name = $type->getName();
271+
$matches = !$type->isBuiltin() && $reason instanceof $name;
267272
}
268273

269274
// If we look for a single match (union), we can return early on match

tests/DeferredTest.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ class DeferredTest extends TestCase
88
{
99
use PromiseTest\FullTestTrait;
1010

11-
public function getPromiseTestAdapter(callable $canceller = null)
11+
public function getPromiseTestAdapter(callable $canceller = null): CallbackPromiseAdapter
1212
{
1313
$d = new Deferred($canceller);
1414

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

2323
/** @test */
24-
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException()
24+
public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithException(): void
2525
{
2626
gc_collect_cycles();
2727
$deferred = new Deferred(function ($resolve, $reject) {
@@ -34,7 +34,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerRejectsWithEx
3434
}
3535

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

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

57-
$deferred = new Deferred(function () use (&$deferred) { });
57+
$deferred = new Deferred(function () use (&$deferred) {
58+
assert($deferred instanceof Deferred);
59+
});
5860
$deferred->reject(new \Exception('foo'));
5961
unset($deferred);
6062

tests/Fiber.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
if (!class_exists(Fiber::class)) {
4+
/**
5+
* Fiber stub to make PHPStan happy on PHP < 8.1
6+
*
7+
* @link https://www.php.net/manual/en/class.fiber.php
8+
* @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/framework-x with permission
9+
*/
10+
class Fiber
11+
{
12+
public static function suspend(mixed $value): void
13+
{
14+
// NOOP
15+
}
16+
17+
public function __construct(callable $callback)
18+
{
19+
assert(is_callable($callback));
20+
}
21+
22+
public function start(): int
23+
{
24+
return 42;
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)