diff --git a/.gitattributes b/.gitattributes index 21be40ca..5d5606da 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 71802ddf..43c9cb3f 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 a5e089c1..79c05834 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,9 @@ Table of Contents * [Rejection forwarding](#rejection-forwarding) * [Mixed resolution and rejection forwarding](#mixed-resolution-and-rejection-forwarding) 5. [Install](#install) -6. [Credits](#credits) -7. [License](#license) +6. [Tests](#tests) +7. [Credits](#credits) +8. [License](#license) Introduction ------------ @@ -586,6 +587,27 @@ PHP versions like this: composer require "react/promise:^3@dev || ^2 || ^1" ``` +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +On top of this, we use PHPStan on level 3 to ensure type safety across the project: + +```bash +vendor/bin/phpstan +``` + Credits ------- diff --git a/composer.json b/composer.json index b91ae987..33eb2a1b 100644 --- a/composer.json +++ b/composer.json @@ -28,13 +28,16 @@ "php": ">=7.1.0" }, "require-dev": { + "phpstan/phpstan": "1.10.20 || 1.4.10", "phpunit/phpunit": "^9.5 || ^7.5" }, "autoload": { "psr-4": { "React\\Promise\\": "src/" }, - "files": ["src/functions_include.php"] + "files": [ + "src/functions_include.php" + ] }, "autoload-dev": { "psr-4": { @@ -42,7 +45,10 @@ "tests/fixtures/", "tests/" ] - } + }, + "files": [ + "tests/Fiber.php" + ] }, "keywords": [ "promise", diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..5b3540e9 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + level: 3 + + paths: + - src/ + - tests/ diff --git a/src/functions.php b/src/functions.php index b8eac495..cf1587d5 100644 --- a/src/functions.php +++ b/src/functions.php @@ -256,12 +256,15 @@ function _checkTypehint(callable $callback, \Throwable $reason): bool if ($type instanceof \ReflectionIntersectionType) { foreach ($type->getTypes() as $typeToMatch) { + assert($typeToMatch instanceof \ReflectionNamedType); if (!($matches = ($typeToMatch->isBuiltin() && \gettype($reason) === $typeToMatch->getName()) || (new \ReflectionClass($typeToMatch->getName()))->isInstance($reason))) { break; } } + assert(isset($matches)); } else { + assert($type instanceof \ReflectionNamedType); $matches = ($type->isBuiltin() && \gettype($reason) === $type->getName()) || (new \ReflectionClass($type->getName()))->isInstance($reason); } diff --git a/tests/DeferredTest.php b/tests/DeferredTest.php index 423625c3..cd732702 100644 --- a/tests/DeferredTest.php +++ b/tests/DeferredTest.php @@ -54,7 +54,9 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc gc_collect_cycles(); gc_collect_cycles(); // clear twice to avoid leftovers in PHP 7.4 with ext-xdebug and code coverage turned on - $deferred = new Deferred(function () use (&$deferred) { }); + $deferred = new Deferred(function () use (&$deferred) { + assert($deferred instanceof Deferred); + }); $deferred->reject(new \Exception('foo')); unset($deferred); diff --git a/tests/Fiber.php b/tests/Fiber.php new file mode 100644 index 00000000..a1b196c1 --- /dev/null +++ b/tests/Fiber.php @@ -0,0 +1,27 @@ +expectCallableNever()); - any([$deferred->promise(), $promise2], 1)->cancel(); + any([$deferred->promise(), $promise2])->cancel(); } } diff --git a/tests/FunctionCheckTypehintTest.php b/tests/FunctionCheckTypehintTest.php index eb3b4a3c..a8e772a3 100644 --- a/tests/FunctionCheckTypehintTest.php +++ b/tests/FunctionCheckTypehintTest.php @@ -122,7 +122,7 @@ public function shouldAcceptStaticClassCallbackWithIntersectionTypehint() public function shouldAcceptInvokableObjectCallbackWithDNFTypehint() { self::assertFalse(_checkTypehint(new CallbackWithDNFTypehintClass(), new \RuntimeException())); - self::assertTrue(_checkTypehint(new CallbackWithDNFTypehintClass(), new ArrayAccessibleException())); + self::assertTrue(_checkTypehint(new CallbackWithDNFTypehintClass(), new IterableException())); self::assertTrue(_checkTypehint(new CallbackWithDNFTypehintClass(), new CountableException())); } @@ -134,7 +134,7 @@ public function shouldAcceptObjectMethodCallbackWithDNFTypehint() { self::assertFalse(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new \RuntimeException())); self::assertTrue(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new CountableException())); - self::assertTrue(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new ArrayAccessibleException())); + self::assertTrue(_checkTypehint([new CallbackWithDNFTypehintClass(), 'testCallback'], new IterableException())); } /** @@ -145,7 +145,7 @@ public function shouldAcceptStaticClassCallbackWithDNFTypehint() { self::assertFalse(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new \RuntimeException())); self::assertTrue(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new CountableException())); - self::assertTrue(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new ArrayAccessibleException())); + self::assertTrue(_checkTypehint([CallbackWithDNFTypehintClass::class, 'testCallbackStatic'], new IterableException())); } /** @test */ diff --git a/tests/PHP8.php b/tests/PHP8.php new file mode 100644 index 00000000..4e2b7d4d --- /dev/null +++ b/tests/PHP8.php @@ -0,0 +1,17 @@ += 80000); + } +} diff --git a/tests/PromiseTest.php b/tests/PromiseTest.php index 49e05400..15a3528d 100644 --- a/tests/PromiseTest.php +++ b/tests/PromiseTest.php @@ -138,6 +138,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerWithReference { gc_collect_cycles(); $promise = new Promise(function () {}, function () use (&$promise) { + assert($promise instanceof Promise); throw new \Exception('foo'); }); $promise->cancel(); @@ -155,6 +156,7 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfResolverWithReferenceT { gc_collect_cycles(); $promise = new Promise(function () use (&$promise) { + assert($promise instanceof Promise); throw new \Exception('foo'); }); unset($promise); @@ -172,7 +174,9 @@ public function shouldRejectWithoutCreatingGarbageCyclesIfCancellerHoldsReferenc gc_collect_cycles(); $promise = new Promise(function () { throw new \Exception('foo'); - }, function () use (&$promise) { }); + }, function () use (&$promise) { + assert($promise instanceof Promise); + }); unset($promise); $this->assertSame(0, gc_collect_cycles()); @@ -249,17 +253,6 @@ public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPro $this->assertSame(0, gc_collect_cycles()); } - /** @test */ - public function shouldNotLeaveGarbageCyclesWhenRemovingLastReferenceToPendingPromiseWithProgressFollowers() - { - gc_collect_cycles(); - $promise = new Promise(function () { }); - $promise->then(null, null, function () { }); - unset($promise); - - $this->assertSame(0, gc_collect_cycles()); - } - /** @test */ public function shouldFulfillIfFullfilledWithSimplePromise() { diff --git a/tests/TestCase.php b/tests/TestCase.php index 5cd4c35c..1de48086 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -38,12 +38,13 @@ public function expectCallableNever(): callable protected function createCallableMock() { - if (method_exists('PHPUnit\Framework\MockObject\MockBuilder', 'addMethods')) { + $builder = $this->getMockBuilder(\stdClass::class); + if (method_exists($builder, 'addMethods')) { // PHPUnit 9+ - return $this->getMockBuilder('stdClass')->addMethods(array('__invoke'))->getMock(); + return $builder->addMethods(['__invoke'])->getMock(); } else { // legacy PHPUnit 4 - PHPUnit 9 - return $this->getMockBuilder('stdClass')->setMethods(array('__invoke'))->getMock(); + return $builder->setMethods(['__invoke'])->getMock(); } } } diff --git a/tests/fixtures/ArrayAccessibleException.php b/tests/fixtures/ArrayAccessibleException.php deleted file mode 100644 index 71b7ad2a..00000000 --- a/tests/fixtures/ArrayAccessibleException.php +++ /dev/null @@ -1,22 +0,0 @@ -