Skip to content

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

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

Merged
merged 2 commits into from
Jun 14, 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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ To run the test suite, go to the project root and run:
vendor/bin/phpunit
```

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

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

## License

MIT, see [LICENSE file](LICENSE).
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react/promise": "^3.0 || ^2.8 || ^1.2.1"
},
"require-dev": {
"phpstan/phpstan": "1.10.18 || 1.4.10",
"phpunit/phpunit": "^9.5 || ^7.5"
},
"autoload": {
Expand Down
11 changes: 11 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
parameters:
level: max

paths:
- src/
- tests/

reportUnmatchedIgnoredErrors: false
ignoreErrors:
# ignore generic usage like `PromiseInterface<T>` until fixed upstream
- '/^PHPDoc .* contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/'
18 changes: 15 additions & 3 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ function await(PromiseInterface $promise)
$resolved = null;
$exception = null;
$rejected = false;

/** @var bool $loopStarted */
$loopStarted = false;

$promise->then(
Expand Down Expand Up @@ -294,7 +296,7 @@ function delay(float $seconds): void
* });
* ```
*
* @param callable(...$args):\Generator<mixed,PromiseInterface,mixed,mixed> $function
* @param callable(mixed ...$args):(\Generator<mixed,PromiseInterface,mixed,mixed>|mixed) $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<mixed>
* @since 3.0.0
Expand All @@ -313,6 +315,7 @@ function coroutine(callable $function, ...$args): PromiseInterface

$promise = null;
$deferred = new Deferred(function () use (&$promise) {
/** @var ?PromiseInterface $promise */
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
Expand All @@ -333,6 +336,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
return;
}

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

assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
Expand All @@ -364,6 +369,7 @@ function coroutine(callable $function, ...$args): PromiseInterface
*/
function parallel(iterable $tasks): PromiseInterface
{
/** @var array<int,PromiseInterface> $pending */
$pending = [];
$deferred = new Deferred(function () use (&$pending) {
foreach ($pending as $promise) {
Expand Down Expand Up @@ -425,6 +431,7 @@ function series(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand All @@ -437,9 +444,9 @@ function series(iterable $tasks): PromiseInterface
assert($tasks instanceof \Iterator);
}

/** @var callable():void $next */
$taskCallback = function ($result) use (&$results, &$next) {
$results[] = $result;
/** @var \Closure $next */
$next();
};

Expand All @@ -453,9 +460,11 @@ function series(iterable $tasks): PromiseInterface
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}

assert(\is_callable($task));
$promise = \call_user_func($task);
assert($promise instanceof PromiseInterface);
$pending = $promise;
Expand All @@ -469,13 +478,14 @@ function series(iterable $tasks): PromiseInterface
}

/**
* @param iterable<callable(mixed=):PromiseInterface<mixed>> $tasks
* @param iterable<(callable():PromiseInterface<mixed>)|(callable(mixed):PromiseInterface<mixed>)> $tasks
* @return PromiseInterface<mixed>
*/
function waterfall(iterable $tasks): PromiseInterface
{
$pending = null;
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
Expand All @@ -498,9 +508,11 @@ function waterfall(iterable $tasks): PromiseInterface
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}

assert(\is_callable($task));
$promise = \call_user_func_array($task, func_get_args());
assert($promise instanceof PromiseInterface);
$pending = $promise;
Expand Down
28 changes: 14 additions & 14 deletions tests/AwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

class AwaitTest extends TestCase
{
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(): void
{
$promise = new Promise(function () {
throw new \Exception('test');
Expand All @@ -19,7 +19,7 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
React\Async\await($promise);
}

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

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

public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError(): void
{
$promise = new Promise(function ($_, $reject) {
throw new \Error('Test', 42);
Expand All @@ -61,7 +61,7 @@ public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
React\Async\await($promise);
}

public function testAwaitReturnsValueWhenPromiseIsFullfilled()
public function testAwaitReturnsValueWhenPromiseIsFullfilled(): void
{
$promise = new Promise(function ($resolve) {
$resolve(42);
Expand All @@ -70,7 +70,7 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled()
$this->assertEquals(42, React\Async\await($promise));
}

public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop(): void
{
$promise = new Promise(function ($resolve) {
Loop::addTimer(0.02, function () use ($resolve) {
Expand All @@ -84,7 +84,7 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto
$this->assertEquals(2, React\Async\await($promise));
}

public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop()
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop(): void
{
$now = true;

Expand All @@ -100,7 +100,7 @@ public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop
$this->assertTrue($now);
}

public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop()
public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop(): void
{
$ticks = 0;

Expand Down Expand Up @@ -128,7 +128,7 @@ public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoo
$this->assertEquals(2, $ticks);
}

public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLastTimerFinishes()
public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLastTimerFinishes(): void
{
$promise = new Promise(function ($resolve) {
Loop::addTimer(0.02, function () use ($resolve) {
Expand Down Expand Up @@ -159,7 +159,7 @@ public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLast
$this->assertEquals(1, $ticks);
}

public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop()
public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop(): void
{
$ticks = 0;

Expand Down Expand Up @@ -191,7 +191,7 @@ public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop
$this->assertEquals(2, $ticks);
}

public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastTimerFinishes()
public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastTimerFinishes(): void
{
$promise = new Promise(function ($_, $reject) {
Loop::addTimer(0.02, function () use (&$reject) {
Expand Down Expand Up @@ -227,7 +227,7 @@ public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastT
$this->assertEquals(1, $ticks);
}

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

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

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