Skip to content

Commit 5c7e785

Browse files
authored
Merge pull request #76 from clue-labs/phpstan-v4
[4.x] Add PHPStan to test environment with `max` level
2 parents 7012b4c + 6185725 commit 5c7e785

17 files changed

+219
-132
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
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
/tests/ export-ignore

.github/workflows/ci.yml

+17
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,20 @@ jobs:
2222
ini-file: development
2323
- run: composer install
2424
- run: vendor/bin/phpunit --coverage-text
25+
26+
PHPStan:
27+
name: PHPStan (PHP ${{ matrix.php }})
28+
runs-on: ubuntu-22.04
29+
strategy:
30+
matrix:
31+
php:
32+
- 8.2
33+
- 8.1
34+
steps:
35+
- uses: actions/checkout@v3
36+
- uses: shivammathur/setup-php@v2
37+
with:
38+
php-version: ${{ matrix.php }}
39+
coverage: none
40+
- run: composer install
41+
- run: vendor/bin/phpstan

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,12 @@ To run the test suite, go to the project root and run:
659659
vendor/bin/phpunit
660660
```
661661

662+
On top of this, we use PHPStan on max level to ensure type safety across the project:
663+
664+
```bash
665+
vendor/bin/phpstan
666+
```
667+
662668
## License
663669

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

composer.json

+1
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",
3435
"phpunit/phpunit": "^9.5"
3536
},
3637
"autoload": {

phpstan.neon.dist

+11
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/FiberMap.php

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,35 +9,43 @@
99
*/
1010
final class FiberMap
1111
{
12+
/** @var array<int,bool> */
1213
private static array $status = [];
13-
private static array $map = [];
1414

15+
/** @var array<int,PromiseInterface> */
16+
private static array $map = [];
17+
18+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
1519
public static function register(\Fiber $fiber): void
1620
{
1721
self::$status[\spl_object_id($fiber)] = false;
18-
self::$map[\spl_object_id($fiber)] = [];
1922
}
2023

24+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
2125
public static function cancel(\Fiber $fiber): void
2226
{
2327
self::$status[\spl_object_id($fiber)] = true;
2428
}
2529

30+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
2631
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
2732
{
2833
self::$map[\spl_object_id($fiber)] = $promise;
2934
}
3035

36+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
3137
public static function unsetPromise(\Fiber $fiber, PromiseInterface $promise): void
3238
{
3339
unset(self::$map[\spl_object_id($fiber)]);
3440
}
3541

42+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
3643
public static function getPromise(\Fiber $fiber): ?PromiseInterface
3744
{
3845
return self::$map[\spl_object_id($fiber)] ?? null;
3946
}
4047

48+
/** @param \Fiber<mixed,mixed,mixed,mixed> $fiber */
4149
public static function unregister(\Fiber $fiber): void
4250
{
4351
unset(self::$status[\spl_object_id($fiber)], self::$map[\spl_object_id($fiber)]);

src/SimpleFiber.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
*/
1010
final class SimpleFiber implements FiberInterface
1111
{
12+
/** @var ?\Fiber<void,void,void,callable(): mixed> */
1213
private static ?\Fiber $scheduler = null;
14+
1315
private static ?\Closure $suspend = null;
16+
17+
/** @var ?\Fiber<mixed,mixed,mixed,mixed> */
1418
private ?\Fiber $fiber = null;
1519

1620
public function __construct()
@@ -57,13 +61,17 @@ public function suspend(): mixed
5761
self::$scheduler = new \Fiber(static fn() => Loop::run());
5862
// Run event loop to completion on shutdown.
5963
\register_shutdown_function(static function (): void {
64+
assert(self::$scheduler instanceof \Fiber);
6065
if (self::$scheduler->isSuspended()) {
6166
self::$scheduler->resume();
6267
}
6368
});
6469
}
6570

66-
return (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start())();
71+
$ret = (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start());
72+
assert(\is_callable($ret));
73+
74+
return $ret();
6775
}
6876

6977
return \Fiber::suspend();

src/functions.php

+20-5
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@
176176
* await($promise);
177177
* ```
178178
*
179-
* @param callable(mixed ...$args):mixed $function
180-
* @return callable(): PromiseInterface<mixed>
179+
* @param callable $function
180+
* @return callable(mixed ...): PromiseInterface<mixed>
181181
* @since 4.0.0
182182
* @see coroutine()
183183
*/
@@ -192,6 +192,7 @@ function async(callable $function): callable
192192
} catch (\Throwable $exception) {
193193
$reject($exception);
194194
} finally {
195+
assert($fiber instanceof \Fiber);
195196
FiberMap::unregister($fiber);
196197
}
197198
});
@@ -200,6 +201,7 @@ function async(callable $function): callable
200201

201202
$fiber->start();
202203
}, function () use (&$fiber): void {
204+
assert($fiber instanceof \Fiber);
203205
FiberMap::cancel($fiber);
204206
$promise = FiberMap::getPromise($fiber);
205207
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
@@ -287,6 +289,7 @@ function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFibe
287289
FiberMap::unsetPromise($lowLevelFiber, $promise);
288290
}
289291

292+
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
290293
if ($fiber === null) {
291294
$resolved = true;
292295
$resolvedValue = $value;
@@ -309,6 +312,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
309312
// what a lovely piece of code!
310313
$r = new \ReflectionProperty('Exception', 'trace');
311314
$trace = $r->getValue($throwable);
315+
assert(\is_array($trace));
312316

313317
// Exception trace arguments only available when zend.exception_ignore_args is not set
314318
// @codeCoverageIgnoreStart
@@ -340,6 +344,7 @@ function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowL
340344
}
341345

342346
if ($rejected) {
347+
assert($rejectedThrowable instanceof \Throwable);
343348
throw $rejectedThrowable;
344349
}
345350

@@ -587,7 +592,7 @@ function delay(float $seconds): void
587592
* });
588593
* ```
589594
*
590-
* @param callable(...$args):\Generator<mixed,PromiseInterface,mixed,mixed> $function
595+
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
591596
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
592597
* @return PromiseInterface<mixed>
593598
* @since 3.0.0
@@ -606,6 +611,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
606611

607612
$promise = null;
608613
$deferred = new Deferred(function () use (&$promise) {
614+
/** @var ?PromiseInterface $promise */
609615
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
610616
$promise->cancel();
611617
}
@@ -626,6 +632,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
626632
return;
627633
}
628634

635+
/** @var mixed $promise */
629636
$promise = $generator->current();
630637
if (!$promise instanceof PromiseInterface) {
631638
$next = null;
@@ -635,6 +642,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
635642
return;
636643
}
637644

645+
assert($next instanceof \Closure);
638646
$promise->then(function ($value) use ($generator, $next) {
639647
$generator->send($value);
640648
$next();
@@ -657,6 +665,7 @@ function coroutine(callable $function, mixed ...$args): PromiseInterface
657665
*/
658666
function parallel(iterable $tasks): PromiseInterface
659667
{
668+
/** @var array<int,PromiseInterface> $pending */
660669
$pending = [];
661670
$deferred = new Deferred(function () use (&$pending) {
662671
foreach ($pending as $promise) {
@@ -718,6 +727,7 @@ function series(iterable $tasks): PromiseInterface
718727
{
719728
$pending = null;
720729
$deferred = new Deferred(function () use (&$pending) {
730+
/** @var ?PromiseInterface $pending */
721731
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
722732
$pending->cancel();
723733
}
@@ -730,9 +740,9 @@ function series(iterable $tasks): PromiseInterface
730740
assert($tasks instanceof \Iterator);
731741
}
732742

733-
/** @var callable():void $next */
734743
$taskCallback = function ($result) use (&$results, &$next) {
735744
$results[] = $result;
745+
/** @var \Closure $next */
736746
$next();
737747
};
738748

@@ -746,9 +756,11 @@ function series(iterable $tasks): PromiseInterface
746756
$task = $tasks->current();
747757
$tasks->next();
748758
} else {
759+
assert(\is_array($tasks));
749760
$task = \array_shift($tasks);
750761
}
751762

763+
assert(\is_callable($task));
752764
$promise = \call_user_func($task);
753765
assert($promise instanceof PromiseInterface);
754766
$pending = $promise;
@@ -762,13 +774,14 @@ function series(iterable $tasks): PromiseInterface
762774
}
763775

764776
/**
765-
* @param iterable<callable(mixed=):PromiseInterface<mixed>> $tasks
777+
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
766778
* @return PromiseInterface<mixed>
767779
*/
768780
function waterfall(iterable $tasks): PromiseInterface
769781
{
770782
$pending = null;
771783
$deferred = new Deferred(function () use (&$pending) {
784+
/** @var ?PromiseInterface $pending */
772785
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
773786
$pending->cancel();
774787
}
@@ -791,9 +804,11 @@ function waterfall(iterable $tasks): PromiseInterface
791804
$task = $tasks->current();
792805
$tasks->next();
793806
} else {
807+
assert(\is_array($tasks));
794808
$task = \array_shift($tasks);
795809
}
796810

811+
assert(\is_callable($task));
797812
$promise = \call_user_func_array($task, func_get_args());
798813
assert($promise instanceof PromiseInterface);
799814
$pending = $promise;

0 commit comments

Comments
 (0)