diff --git a/.gitattributes b/.gitattributes index 21be40c..5d5606d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71802dd..43c9cb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 5c13bc5..1453a09 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/composer.json b/composer.json index 2729de2..e208dd3 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..b7f8ddb --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +parameters: + level: max + + paths: + - src/ + - tests/ + + reportUnmatchedIgnoredErrors: false + ignoreErrors: + # ignore generic usage like `PromiseInterface` until fixed upstream + - '/^PHPDoc .* contains generic type React\\Promise\\PromiseInterface<.+> but interface React\\Promise\\PromiseInterface is not generic\.$/' diff --git a/src/functions.php b/src/functions.php index 28c16d0..0096496 100644 --- a/src/functions.php +++ b/src/functions.php @@ -56,6 +56,8 @@ function await(PromiseInterface $promise) $resolved = null; $exception = null; $rejected = false; + + /** @var bool $loopStarted */ $loopStarted = false; $promise->then( @@ -294,7 +296,7 @@ function delay(float $seconds): void * }); * ``` * - * @param callable(...$args):\Generator $function + * @param callable(mixed ...$args):(\Generator|mixed) $function * @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is * @return PromiseInterface * @since 3.0.0 @@ -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(); } @@ -333,6 +336,7 @@ function coroutine(callable $function, ...$args): PromiseInterface return; } + /** @var mixed $promise */ $promise = $generator->current(); if (!$promise instanceof PromiseInterface) { $next = null; @@ -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(); @@ -364,6 +369,7 @@ function coroutine(callable $function, ...$args): PromiseInterface */ function parallel(iterable $tasks): PromiseInterface { + /** @var array $pending */ $pending = []; $deferred = new Deferred(function () use (&$pending) { foreach ($pending as $promise) { @@ -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(); } @@ -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(); }; @@ -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; @@ -469,13 +478,14 @@ function series(iterable $tasks): PromiseInterface } /** - * @param iterable> $tasks + * @param iterable<(callable():PromiseInterface)|(callable(mixed):PromiseInterface)> $tasks * @return PromiseInterface */ 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(); } @@ -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; diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index da9079a..397a0fa 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -8,7 +8,7 @@ class AwaitTest extends TestCase { - public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException() + public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(): void { $promise = new Promise(function () { throw new \Exception('test'); @@ -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'); @@ -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'); @@ -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); @@ -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); @@ -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) { @@ -84,7 +84,7 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto $this->assertEquals(2, React\Async\await($promise)); } - public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop() + public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop(): void { $now = true; @@ -100,7 +100,7 @@ public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop $this->assertTrue($now); } - public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop() + public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop(): void { $ticks = 0; @@ -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) { @@ -159,7 +159,7 @@ public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLast $this->assertEquals(1, $ticks); } - public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop() + public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop(): void { $ticks = 0; @@ -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) { @@ -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'); @@ -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'); @@ -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'); diff --git a/tests/CoroutineTest.php b/tests/CoroutineTest.php index adc82bc..c9b7439 100644 --- a/tests/CoroutineTest.php +++ b/tests/CoroutineTest.php @@ -9,7 +9,7 @@ class CoroutineTest extends TestCase { - public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsWithoutGenerator() + public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsWithoutGenerator(): void { $promise = coroutine(function () { return 42; @@ -18,10 +18,10 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsWithoutGene $promise->then($this->expectCallableOnceWith(42)); } - public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately() + public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately(): void { $promise = coroutine(function () { - if (false) { + if (false) { // @phpstan-ignore-line yield; } return 42; @@ -30,7 +30,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsImmediately $promise->then($this->expectCallableOnceWith(42)); } - public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldingPromise() + public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldingPromise(): void { $promise = coroutine(function () { $value = yield resolve(42); @@ -40,7 +40,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi $promise->then($this->expectCallableOnceWith(42)); } - public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsWithoutGenerator() + public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsWithoutGenerator(): void { $promise = coroutine(function () { throw new \RuntimeException('Foo'); @@ -49,10 +49,10 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsWithoutGenera $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Foo'))); } - public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately() + public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately(): void { $promise = coroutine(function () { - if (false) { + if (false) { // @phpstan-ignore-line yield; } throw new \RuntimeException('Foo'); @@ -61,7 +61,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsImmediately() $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Foo'))); } - public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYieldingPromise() + public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYieldingPromise(): void { $promise = coroutine(function () { $reason = yield resolve('Foo'); @@ -71,7 +71,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYielding $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Foo'))); } - public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYieldingRejectedPromise() + public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYieldingRejectedPromise(): void { $promise = coroutine(function () { try { @@ -84,7 +84,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionThrowsAfterYielding $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Foo'))); } - public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldingRejectedPromise() + public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldingRejectedPromise(): void { $promise = coroutine(function () { try { @@ -97,7 +97,7 @@ public function testCoroutineReturnsFulfilledPromiseIfFunctionReturnsAfterYieldi $promise->then($this->expectCallableOnceWith(42)); } - public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue() + public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(): void { $promise = coroutine(function () { yield 42; @@ -106,7 +106,7 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue( $promise->then(null, $this->expectCallableOnceWith(new \UnexpectedValueException('Expected coroutine to yield React\Promise\PromiseInterface, but got integer'))); } - public function testCancelCoroutineWillReturnRejectedPromiseWhenCancellingPendingPromiseRejects() + public function testCancelCoroutineWillReturnRejectedPromiseWhenCancellingPendingPromiseRejects(): void { $promise = coroutine(function () { yield new Promise(function () { }, function () { @@ -114,12 +114,13 @@ public function testCancelCoroutineWillReturnRejectedPromiseWhenCancellingPendin }); }); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Operation cancelled'))); } - public function testCancelCoroutineWillReturnFulfilledPromiseWhenCancellingPendingPromiseRejectsInsideCatchThatReturnsValue() + public function testCancelCoroutineWillReturnFulfilledPromiseWhenCancellingPendingPromiseRejectsInsideCatchThatReturnsValue(): void { $promise = coroutine(function () { try { @@ -131,12 +132,13 @@ public function testCancelCoroutineWillReturnFulfilledPromiseWhenCancellingPendi } }); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then($this->expectCallableOnceWith(42)); } - public function testCancelCoroutineWillReturnPendigPromiseWhenCancellingFirstPromiseRejectsInsideCatchThatYieldsSecondPromise() + public function testCancelCoroutineWillReturnPendigPromiseWhenCancellingFirstPromiseRejectsInsideCatchThatYieldsSecondPromise(): void { $promise = coroutine(function () { try { @@ -150,12 +152,13 @@ public function testCancelCoroutineWillReturnPendigPromiseWhenCancellingFirstPro } }); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } - public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorReturns() + public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorReturns(): void { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -165,7 +168,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet gc_collect_cycles(); $promise = coroutine(function () { - if (false) { + if (false) { // @phpstan-ignore-line yield; } return 42; @@ -176,7 +179,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorRet $this->assertEquals(0, gc_collect_cycles()); } - public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithExceptionImmediately() + public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithExceptionImmediately(): void { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -195,7 +198,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseReject $this->assertEquals(0, gc_collect_cycles()); } - public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithExceptionOnCancellation() + public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithExceptionOnCancellation(): void { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -209,13 +212,14 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesForPromiseReject }); }); + assert(method_exists($promise, 'cancel')); $promise->cancel(); unset($promise); $this->assertEquals(0, gc_collect_cycles()); } - public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorThrowsBeforeFirstYield() + public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorThrowsBeforeFirstYield(): void { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); @@ -225,7 +229,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorThr $promise = coroutine(function () { throw new \RuntimeException('Failed', 42); - yield; + yield; // @phpstan-ignore-line }); unset($promise); @@ -233,7 +237,7 @@ public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorThr $this->assertEquals(0, gc_collect_cycles()); } - public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYieldsInvalidValue() + public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorYieldsInvalidValue(): void { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); diff --git a/tests/DelayTest.php b/tests/DelayTest.php index d34c16e..0091943 100644 --- a/tests/DelayTest.php +++ b/tests/DelayTest.php @@ -8,7 +8,7 @@ class DelayTest extends TestCase { - public function testDelayBlocksForGivenPeriod() + public function testDelayBlocksForGivenPeriod(): void { $time = microtime(true); delay(0.02); @@ -17,7 +17,7 @@ public function testDelayBlocksForGivenPeriod() $this->assertEqualsWithDelta(0.02, $time, 0.01); } - public function testDelaySmallPeriodBlocksForCloseToZeroSeconds() + public function testDelaySmallPeriodBlocksForCloseToZeroSeconds(): void { $time = microtime(true); delay(0.000001); @@ -26,7 +26,7 @@ public function testDelaySmallPeriodBlocksForCloseToZeroSeconds() $this->assertLessThan(0.01, $time); } - public function testDelayNegativePeriodBlocksForCloseToZeroSeconds() + public function testDelayNegativePeriodBlocksForCloseToZeroSeconds(): void { $time = microtime(true); delay(-1); @@ -35,7 +35,7 @@ public function testDelayNegativePeriodBlocksForCloseToZeroSeconds() $this->assertLessThan(0.01, $time); } - public function testDelayRunsOtherEventsWhileWaiting() + public function testDelayRunsOtherEventsWhileWaiting(): void { $buffer = 'a'; Loop::addTimer(0.001, function () use (&$buffer) { diff --git a/tests/ParallelTest.php b/tests/ParallelTest.php index 1a5759b..c1ed553 100644 --- a/tests/ParallelTest.php +++ b/tests/ParallelTest.php @@ -6,10 +6,11 @@ use React\EventLoop\Loop; use React\Promise\Promise; use function React\Promise\reject; +use function React\Promise\resolve; class ParallelTest extends TestCase { - public function testParallelWithoutTasks() + public function testParallelWithoutTasks(): void { $tasks = array(); @@ -18,11 +19,11 @@ public function testParallelWithoutTasks() $promise->then($this->expectCallableOnceWith(array())); } - public function testParallelWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray() + public function testParallelWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray(): void { $tasks = (function () { - if (false) { - yield; + if (false) { // @phpstan-ignore-line + yield function () { return resolve(null); }; } })(); @@ -31,7 +32,7 @@ public function testParallelWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray $promise->then($this->expectCallableOnceWith([])); } - public function testParallelWithTasks() + public function testParallelWithTasks(): void { $tasks = array( function () { @@ -63,7 +64,7 @@ function () { $timer->assertInRange(0.1, 0.2); } - public function testParallelWithTasksFromGeneratorResolvesWithArrayOfFulfillmentValues() + public function testParallelWithTasksFromGeneratorResolvesWithArrayOfFulfillmentValues(): void { $tasks = (function () { yield function () { @@ -95,7 +96,7 @@ public function testParallelWithTasksFromGeneratorResolvesWithArrayOfFulfillment $timer->assertInRange(0.1, 0.2); } - public function testParallelWithErrorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testParallelWithErrorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $called = 0; @@ -127,12 +128,12 @@ function () use (&$called) { $this->assertSame(2, $called); } - public function testParallelWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testParallelWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $called = 0; $tasks = (function () use (&$called) { - while (true) { + while (true) { // @phpstan-ignore-line yield function () use (&$called) { return reject(new \RuntimeException('Rejected ' . ++$called)); }; @@ -146,7 +147,7 @@ public function testParallelWithErrorFromInfiniteGeneratorReturnsPromiseRejected $this->assertSame(1, $called); } - public function testParallelWithErrorWillCancelPendingPromises() + public function testParallelWithErrorWillCancelPendingPromises(): void { $cancelled = 0; @@ -175,7 +176,7 @@ function () use (&$cancelled) { $this->assertSame(1, $cancelled); } - public function testParallelWillCancelPendingPromisesWhenCallingCancelOnResultingPromise() + public function testParallelWillCancelPendingPromisesWhenCallingCancelOnResultingPromise(): void { $cancelled = 0; @@ -193,12 +194,13 @@ function () use (&$cancelled) { ); $promise = React\Async\parallel($tasks); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $this->assertSame(2, $cancelled); } - public function testParallelWithDelayedErrorReturnsPromiseRejectedWithExceptionFromTask() + public function testParallelWithDelayedErrorReturnsPromiseRejectedWithExceptionFromTask(): void { $called = 0; diff --git a/tests/SeriesTest.php b/tests/SeriesTest.php index 404c907..25aa104 100644 --- a/tests/SeriesTest.php +++ b/tests/SeriesTest.php @@ -6,10 +6,11 @@ use React\EventLoop\Loop; use React\Promise\Promise; use function React\Promise\reject; +use function React\Promise\resolve; class SeriesTest extends TestCase { - public function testSeriesWithoutTasks() + public function testSeriesWithoutTasks(): void { $tasks = array(); @@ -18,11 +19,11 @@ public function testSeriesWithoutTasks() $promise->then($this->expectCallableOnceWith(array())); } - public function testSeriesWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray() + public function testSeriesWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray(): void { $tasks = (function () { - if (false) { - yield; + if (false) { // @phpstan-ignore-line + yield function () { return resolve(null); }; } })(); @@ -31,7 +32,7 @@ public function testSeriesWithoutTasksFromEmptyGeneratorResolvesWithEmptyArray() $promise->then($this->expectCallableOnceWith([])); } - public function testSeriesWithTasks() + public function testSeriesWithTasks(): void { $tasks = array( function () { @@ -63,7 +64,7 @@ function () { $timer->assertInRange(0.10, 0.20); } - public function testSeriesWithTasksFromGeneratorResolvesWithArrayOfFulfillmentValues() + public function testSeriesWithTasksFromGeneratorResolvesWithArrayOfFulfillmentValues(): void { $tasks = (function () { yield function () { @@ -95,7 +96,7 @@ public function testSeriesWithTasksFromGeneratorResolvesWithArrayOfFulfillmentVa $timer->assertInRange(0.10, 0.20); } - public function testSeriesWithError() + public function testSeriesWithError(): void { $called = 0; @@ -126,12 +127,12 @@ function () use (&$called) { $this->assertSame(1, $called); } - public function testSeriesWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testSeriesWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $called = 0; $tasks = (function () use (&$called) { - while (true) { + while (true) { // @phpstan-ignore-line yield function () use (&$called) { return reject(new \RuntimeException('Rejected ' . ++$called)); }; @@ -145,14 +146,15 @@ public function testSeriesWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWi $this->assertSame(1, $called); } - public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testSeriesWithErrorFromInfiniteIteratorAggregateReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $tasks = new class() implements \IteratorAggregate { + /** @var int */ public $called = 0; public function getIterator(): \Iterator { - while (true) { + while (true) { // @phpstan-ignore-line yield function () { return reject(new \RuntimeException('Rejected ' . ++$this->called)); }; @@ -167,7 +169,7 @@ public function getIterator(): \Iterator $this->assertSame(1, $tasks->called); } - public function testSeriesWillCancelFirstPendingPromiseWhenCallingCancelOnResultingPromise() + public function testSeriesWillCancelFirstPendingPromiseWhenCallingCancelOnResultingPromise(): void { $cancelled = 0; @@ -185,6 +187,7 @@ function () use (&$cancelled) { ); $promise = React\Async\series($tasks); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $this->assertSame(1, $cancelled); diff --git a/tests/TestCase.php b/tests/TestCase.php index ee0f476..c7bf729 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,45 +3,43 @@ namespace React\Tests\Async; use PHPUnit\Framework\MockObject\MockBuilder; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase { - protected function expectCallableOnce() + protected function expectCallableOnce(): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke'); + $mock->expects($this->once())->method('__invoke'); + assert(is_callable($mock)); return $mock; } - protected function expectCallableOnceWith($value) + /** @param mixed $value */ + protected function expectCallableOnceWith($value): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($value); + $mock->expects($this->once())->method('__invoke')->with($value); + assert(is_callable($mock)); return $mock; } - protected function expectCallableNever() + protected function expectCallableNever(): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->never()) - ->method('__invoke'); + $mock->expects($this->never())->method('__invoke'); + assert(is_callable($mock)); return $mock; } - protected function createCallableMock() + protected function createCallableMock(): MockObject { if (method_exists(MockBuilder::class, 'addMethods')) { - // PHPUnit 9+ + // @phpstan-ignore-next-line PHPUnit 9+ return $this->getMockBuilder(\stdClass::class)->addMethods(['__invoke'])->getMock(); } else { // PHPUnit < 9 diff --git a/tests/Timer.php b/tests/Timer.php index 0a37a73..3e381cc 100644 --- a/tests/Timer.php +++ b/tests/Timer.php @@ -4,8 +4,13 @@ class Timer { + /** @var TestCase */ private $testCase; + + /** @var float */ private $start; + + /** @var float */ private $stop; public function __construct(TestCase $testCase) @@ -13,32 +18,32 @@ public function __construct(TestCase $testCase) $this->testCase = $testCase; } - public function start() + public function start(): void { $this->start = microtime(true); } - public function stop() + public function stop(): void { $this->stop = microtime(true); } - public function getInterval() + public function getInterval(): float { return $this->stop - $this->start; } - public function assertLessThan($milliseconds) + public function assertLessThan(float $milliseconds): void { $this->testCase->assertLessThan($milliseconds, $this->getInterval()); } - public function assertGreaterThan($milliseconds) + public function assertGreaterThan(float $milliseconds): void { $this->testCase->assertGreaterThan($milliseconds, $this->getInterval()); } - public function assertInRange($minMs, $maxMs) + public function assertInRange(float $minMs, float $maxMs): void { $this->assertGreaterThan($minMs); $this->assertLessThan($maxMs); diff --git a/tests/WaterfallTest.php b/tests/WaterfallTest.php index 2fbbc23..8aa6c6f 100644 --- a/tests/WaterfallTest.php +++ b/tests/WaterfallTest.php @@ -6,10 +6,11 @@ use React\EventLoop\Loop; use React\Promise\Promise; use function React\Promise\reject; +use function React\Promise\resolve; class WaterfallTest extends TestCase { - public function testWaterfallWithoutTasks() + public function testWaterfallWithoutTasks(): void { $tasks = array(); @@ -18,11 +19,11 @@ public function testWaterfallWithoutTasks() $promise->then($this->expectCallableOnceWith(null)); } - public function testWaterfallWithoutTasksFromEmptyGeneratorResolvesWithNull() + public function testWaterfallWithoutTasksFromEmptyGeneratorResolvesWithNull(): void { $tasks = (function () { - if (false) { - yield; + if (false) { // @phpstan-ignore-line + yield function () { return resolve(null); }; } })(); @@ -31,7 +32,7 @@ public function testWaterfallWithoutTasksFromEmptyGeneratorResolvesWithNull() $promise->then($this->expectCallableOnceWith(null)); } - public function testWaterfallWithTasks() + public function testWaterfallWithTasks(): void { $tasks = array( function ($foo = 'foo') { @@ -70,7 +71,7 @@ function ($bar) { $timer->assertInRange(0.15, 0.30); } - public function testWaterfallWithTasksFromGeneratorResolvesWithFinalFulfillmentValue() + public function testWaterfallWithTasksFromGeneratorResolvesWithFinalFulfillmentValue(): void { $tasks = (function () { yield function ($foo = 'foo') { @@ -109,7 +110,7 @@ public function testWaterfallWithTasksFromGeneratorResolvesWithFinalFulfillmentV $timer->assertInRange(0.15, 0.30); } - public function testWaterfallWithError() + public function testWaterfallWithError(): void { $called = 0; @@ -140,12 +141,12 @@ function () use (&$called) { $this->assertSame(1, $called); } - public function testWaterfallWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testWaterfallWithErrorFromInfiniteGeneratorReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $called = 0; $tasks = (function () use (&$called) { - while (true) { + while (true) { // @phpstan-ignore-line yield function () use (&$called) { return reject(new \RuntimeException('Rejected ' . ++$called)); }; @@ -159,14 +160,15 @@ public function testWaterfallWithErrorFromInfiniteGeneratorReturnsPromiseRejecte $this->assertSame(1, $called); } - public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks() + public function testWaterfallWithErrorFromInfiniteIteratorAggregateReturnsPromiseRejectedWithExceptionFromTaskAndStopsCallingAdditionalTasks(): void { $tasks = new class() implements \IteratorAggregate { + /** @var int */ public $called = 0; public function getIterator(): \Iterator { - while (true) { + while (true) { // @phpstan-ignore-line yield function () { return reject(new \RuntimeException('Rejected ' . ++$this->called)); }; @@ -181,7 +183,7 @@ public function getIterator(): \Iterator $this->assertSame(1, $tasks->called); } - public function testWaterfallWillCancelFirstPendingPromiseWhenCallingCancelOnResultingPromise() + public function testWaterfallWillCancelFirstPendingPromiseWhenCallingCancelOnResultingPromise(): void { $cancelled = 0; @@ -199,6 +201,7 @@ function () use (&$cancelled) { ); $promise = React\Async\waterfall($tasks); + assert(method_exists($promise, 'cancel')); $promise->cancel(); $this->assertSame(1, $cancelled);