Skip to content

Commit 325ad7b

Browse files
authored
Merge pull request #77 from clue-labs/phpstan-v3
[3.x] Add PHPStan to test environment with `max` level
2 parents 73ee629 + 9b92ce5 commit 325ad7b

14 files changed

+167
-99
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: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,12 @@ To run the test suite, go to the project root and run:
426426
vendor/bin/phpunit
427427
```
428428

429+
On top of this, we use PHPStan on max level to ensure type safety across the project:
430+
431+
```bash
432+
vendor/bin/phpstan
433+
```
434+
429435
## License
430436

431437
MIT, see [LICENSE file](LICENSE).

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"react/promise": "^3.0 || ^2.8 || ^1.2.1"
3232
},
3333
"require-dev": {
34+
"phpstan/phpstan": "1.10.18 || 1.4.10",
3435
"phpunit/phpunit": "^9.5 || ^7.5"
3536
},
3637
"autoload": {

phpstan.neon.dist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
parameters:
2+
level: max
3+
4+
paths:
5+
- src/
6+
- tests/
7+
8+
reportUnmatchedIgnoredErrors: false
9+
ignoreErrors:
10+
# ignore generic usage like `PromiseInterface<T>` until fixed upstream
11+
- '/^PHPDoc .* contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/'

src/functions.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ function await(PromiseInterface $promise)
5656
$resolved = null;
5757
$exception = null;
5858
$rejected = false;
59+
60+
/** @var bool $loopStarted */
5961
$loopStarted = false;
6062

6163
$promise->then(
@@ -294,7 +296,7 @@ function delay(float $seconds): void
294296
* });
295297
* ```
296298
*
297-
* @param callable(...$args):\Generator<mixed,PromiseInterface,mixed,mixed> $function
299+
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
298300
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
299301
* @return PromiseInterface<mixed>
300302
* @since 3.0.0
@@ -313,6 +315,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
313315

314316
$promise = null;
315317
$deferred = new Deferred(function () use (&$promise) {
318+
/** @var ?PromiseInterface $promise */
316319
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
317320
$promise->cancel();
318321
}
@@ -333,6 +336,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
333336
return;
334337
}
335338

339+
/** @var mixed $promise */
336340
$promise = $generator->current();
337341
if (!$promise instanceof PromiseInterface) {
338342
$next = null;
@@ -342,6 +346,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
342346
return;
343347
}
344348

349+
assert($next instanceof \Closure);
345350
$promise->then(function ($value) use ($generator, $next) {
346351
$generator->send($value);
347352
$next();
@@ -364,6 +369,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
364369
*/
365370
function parallel(iterable $tasks): PromiseInterface
366371
{
372+
/** @var array<int,PromiseInterface> $pending */
367373
$pending = [];
368374
$deferred = new Deferred(function () use (&$pending) {
369375
foreach ($pending as $promise) {
@@ -425,6 +431,7 @@ function series(iterable $tasks): PromiseInterface
425431
{
426432
$pending = null;
427433
$deferred = new Deferred(function () use (&$pending) {
434+
/** @var ?PromiseInterface $pending */
428435
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
429436
$pending->cancel();
430437
}
@@ -437,9 +444,9 @@ function series(iterable $tasks): PromiseInterface
437444
assert($tasks instanceof \Iterator);
438445
}
439446

440-
/** @var callable():void $next */
441447
$taskCallback = function ($result) use (&$results, &$next) {
442448
$results[] = $result;
449+
/** @var \Closure $next */
443450
$next();
444451
};
445452

@@ -453,9 +460,11 @@ function series(iterable $tasks): PromiseInterface
453460
$task = $tasks->current();
454461
$tasks->next();
455462
} else {
463+
assert(\is_array($tasks));
456464
$task = \array_shift($tasks);
457465
}
458466

467+
assert(\is_callable($task));
459468
$promise = \call_user_func($task);
460469
assert($promise instanceof PromiseInterface);
461470
$pending = $promise;
@@ -469,13 +478,14 @@ function series(iterable $tasks): PromiseInterface
469478
}
470479

471480
/**
472-
* @param iterable<callable(mixed=):PromiseInterface<mixed>> $tasks
481+
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
473482
* @return PromiseInterface<mixed>
474483
*/
475484
function waterfall(iterable $tasks): PromiseInterface
476485
{
477486
$pending = null;
478487
$deferred = new Deferred(function () use (&$pending) {
488+
/** @var ?PromiseInterface $pending */
479489
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
480490
$pending->cancel();
481491
}
@@ -498,9 +508,11 @@ function waterfall(iterable $tasks): PromiseInterface
498508
$task = $tasks->current();
499509
$tasks->next();
500510
} else {
511+
assert(\is_array($tasks));
501512
$task = \array_shift($tasks);
502513
}
503514

515+
assert(\is_callable($task));
504516
$promise = \call_user_func_array($task, func_get_args());
505517
assert($promise instanceof PromiseInterface);
506518
$pending = $promise;

tests/AwaitTest.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
class AwaitTest extends TestCase
1010
{
11-
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
11+
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(): void
1212
{
1313
$promise = new Promise(function () {
1414
throw new \Exception('test');
@@ -19,7 +19,7 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
1919
React\Async\await($promise);
2020
}
2121

22-
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse()
22+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse(): void
2323
{
2424
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
2525
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
@@ -34,7 +34,7 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith
3434
React\Async\await($promise);
3535
}
3636

37-
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull()
37+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull(): void
3838
{
3939
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
4040
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
@@ -49,7 +49,7 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith
4949
React\Async\await($promise);
5050
}
5151

52-
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
52+
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError(): void
5353
{
5454
$promise = new Promise(function ($_, $reject) {
5555
throw new \Error('Test', 42);
@@ -61,7 +61,7 @@ public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
6161
React\Async\await($promise);
6262
}
6363

64-
public function testAwaitReturnsValueWhenPromiseIsFullfilled()
64+
public function testAwaitReturnsValueWhenPromiseIsFullfilled(): void
6565
{
6666
$promise = new Promise(function ($resolve) {
6767
$resolve(42);
@@ -70,7 +70,7 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled()
7070
$this->assertEquals(42, React\Async\await($promise));
7171
}
7272

73-
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
73+
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop(): void
7474
{
7575
$promise = new Promise(function ($resolve) {
7676
Loop::addTimer(0.02, function () use ($resolve) {
@@ -84,7 +84,7 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto
8484
$this->assertEquals(2, React\Async\await($promise));
8585
}
8686

87-
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop()
87+
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop(): void
8888
{
8989
$now = true;
9090

@@ -100,7 +100,7 @@ public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop
100100
$this->assertTrue($now);
101101
}
102102

103-
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop()
103+
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop(): void
104104
{
105105
$ticks = 0;
106106

@@ -128,7 +128,7 @@ public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoo
128128
$this->assertEquals(2, $ticks);
129129
}
130130

131-
public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLastTimerFinishes()
131+
public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLastTimerFinishes(): void
132132
{
133133
$promise = new Promise(function ($resolve) {
134134
Loop::addTimer(0.02, function () use ($resolve) {
@@ -159,7 +159,7 @@ public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLast
159159
$this->assertEquals(1, $ticks);
160160
}
161161

162-
public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop()
162+
public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop(): void
163163
{
164164
$ticks = 0;
165165

@@ -191,7 +191,7 @@ public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop
191191
$this->assertEquals(2, $ticks);
192192
}
193193

194-
public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastTimerFinishes()
194+
public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastTimerFinishes(): void
195195
{
196196
$promise = new Promise(function ($_, $reject) {
197197
Loop::addTimer(0.02, function () use (&$reject) {
@@ -227,7 +227,7 @@ public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastT
227227
$this->assertEquals(1, $ticks);
228228
}
229229

230-
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
230+
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise(): void
231231
{
232232
if (class_exists('React\Promise\When')) {
233233
$this->markTestSkipped('Not supported on legacy Promise v1 API');
@@ -244,7 +244,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
244244
$this->assertEquals(0, gc_collect_cycles());
245245
}
246246

247-
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
247+
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise(): void
248248
{
249249
if (class_exists('React\Promise\When')) {
250250
$this->markTestSkipped('Not supported on legacy Promise v1 API');
@@ -265,7 +265,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
265265
$this->assertEquals(0, gc_collect_cycles());
266266
}
267267

268-
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
268+
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue(): void
269269
{
270270
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
271271
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');

0 commit comments

Comments
 (0)