From ae8efe4d8ff34487da54f654ffd6f5cdd61cb2c3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Mon, 12 Feb 2024 20:53:12 +0100 Subject: [PATCH] [WIP][3.x] Introduce PHPStan Full commit message will follow --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ composer.json | 1 + phpstan.neon.dist | 10 ++++++++++ src/ExtEvLoop.php | 16 +++++++++++++--- src/ExtEventLoop.php | 4 ++++ src/ExtUvLoop.php | 16 ++++++++++++++++ src/Loop.php | 16 ++++++++++++++++ src/SignalsHandler.php | 3 +++ src/StreamSelectLoop.php | 15 ++++++++++++--- tests/ExtEventLoopTest.php | 9 --------- tests/StreamSelectLoopTest.php | 10 +++++++--- tests/bin/12-undefined.php | 5 +++++ tests/bin/22-stop-uncaught.php | 5 +++++ 13 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4bf2b84..fb6353bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,3 +120,36 @@ jobs: uses: docker://hhvm/hhvm:3.30-lts-latest with: args: hhvm vendor/bin/phpunit + + PHPStan: + name: PHPStan (PHP ${{ matrix.php }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.3 + - 8.2 + - 8.1 + - 8.0 + - 7.4 + - 7.3 + - 7.2 + - 7.1 + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + ini-file: development + ini-values: disable_functions='' # do not disable PCNTL functions on PHP < 8.1 + extensions: sockets, pcntl ${{ matrix.php >= 5.6 && ', event' || '' }} ${{ matrix.php >= 5.4 && ', ev' || '' }} + env: + fail-fast: true # fail step if any extension can not be installed + - name: Install ext-uv on PHP 7+ + run: | + sudo apt-get update -q && sudo apt-get install libuv1-dev + echo "yes" | sudo pecl install ${{ matrix.php >= 8.0 && 'uv-0.3.0' || 'uv-0.2.4' }} + php -m | grep -q uv || echo "extension=uv.so" >> "$(php -r 'echo php_ini_loaded_file();')" + - run: composer install + - run: vendor/bin/phpstan diff --git a/composer.json b/composer.json index 25a41fe1..c840d529 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "php": ">=5.3.0" }, "require-dev": { + "phpstan/phpstan": "^1", "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" }, "suggest": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..fd9e9ea5 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +parameters: + level: max + + paths: + - src/ + - tests/ + +# ignoreErrors: +# - '#type specified#' +# - '#expects resource, resource\|false given#' diff --git a/src/ExtEvLoop.php b/src/ExtEvLoop.php index a3fcec68..ca601cc2 100644 --- a/src/ExtEvLoop.php +++ b/src/ExtEvLoop.php @@ -34,7 +34,7 @@ class ExtEvLoop implements LoopInterface private $futureTickQueue; /** - * @var SplObjectStorage + * @var SplObjectStorage */ private $timers; @@ -192,6 +192,10 @@ public function run() $this->futureTickQueue->tick(); $hasPendingCallbacks = !$this->futureTickQueue->isEmpty(); + /** + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ $wasJustStopped = !$this->running; $nothingLeftToDo = !$this->readStreams && !$this->writeStreams @@ -199,6 +203,10 @@ public function run() && $this->signals->isEmpty(); $flags = Ev::RUN_ONCE; + /** + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ if ($wasJustStopped || $hasPendingCallbacks) { $flags |= Ev::RUN_NOWAIT; } elseif ($nothingLeftToDo) { @@ -222,11 +230,13 @@ public function __destruct() } foreach ($this->readStreams as $key => $stream) { - $this->removeReadStream($key); + $this->readStreams[$key]->stop(); + unset($this->readStreams[$key]); } foreach ($this->writeStreams as $key => $stream) { - $this->removeWriteStream($key); + $this->readStreams[$key]->stop(); + unset($this->readStreams[$key]); } } diff --git a/src/ExtEventLoop.php b/src/ExtEventLoop.php index b162a402..3e720628 100644 --- a/src/ExtEventLoop.php +++ b/src/ExtEventLoop.php @@ -196,6 +196,10 @@ public function run() $this->futureTickQueue->tick(); $flags = EventBase::LOOP_ONCE; + /** + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ if (!$this->running || !$this->futureTickQueue->isEmpty()) { $flags |= EventBase::LOOP_NONBLOCK; } elseif (!$this->readEvents && !$this->writeEvents && !$this->timerEvents->count() && $this->signals->isEmpty()) { diff --git a/src/ExtUvLoop.php b/src/ExtUvLoop.php index 4434720d..3a20265d 100644 --- a/src/ExtUvLoop.php +++ b/src/ExtUvLoop.php @@ -213,6 +213,10 @@ public function run() $this->futureTickQueue->tick(); $hasPendingCallbacks = !$this->futureTickQueue->isEmpty(); + /** + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ $wasJustStopped = !$this->running; $nothingLeftToDo = !$this->readStreams && !$this->writeStreams @@ -223,12 +227,20 @@ public function run() // otherwise use UV::RUN_NOWAIT. // @link http://docs.libuv.org/en/v1.x/loop.html#c.uv_run $flags = \UV::RUN_ONCE; + /** + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ if ($wasJustStopped || $hasPendingCallbacks) { $flags = \UV::RUN_NOWAIT; } elseif ($nothingLeftToDo) { break; } + /** + * @link https://github.com/JetBrains/phpstorm-stubs/pull/1614 + * @phpstan-ignore-next-line + */ \uv_run($this->uv, $flags); } } @@ -261,6 +273,10 @@ private function removeStream($stream) if (!isset($this->readStreams[(int) $stream]) && !isset($this->writeStreams[(int) $stream])) { \uv_poll_stop($this->streamEvents[(int) $stream]); + /** + * @link https://github.com/JetBrains/phpstorm-stubs/pull/1615 + * @phpstan-ignore-next-line + */ \uv_close($this->streamEvents[(int) $stream]); unset($this->streamEvents[(int) $stream]); return; diff --git a/src/Loop.php b/src/Loop.php index 10976eea..111f4bb7 100644 --- a/src/Loop.php +++ b/src/Loop.php @@ -87,6 +87,7 @@ public static function addReadStream($stream, $listener) if (self::$instance === null) { self::get(); } + assert(self::$instance instanceof LoopInterface); self::$instance->addReadStream($stream, $listener); } @@ -105,6 +106,9 @@ public static function addWriteStream($stream, $listener) if (self::$instance === null) { self::get(); } + + assert(self::$instance instanceof LoopInterface); + self::$instance->addWriteStream($stream, $listener); } @@ -150,6 +154,9 @@ public static function addTimer($interval, $callback) if (self::$instance === null) { self::get(); } + + assert(self::$instance instanceof LoopInterface); + return self::$instance->addTimer($interval, $callback); } @@ -167,6 +174,9 @@ public static function addPeriodicTimer($interval, $callback) if (self::$instance === null) { self::get(); } + + assert(self::$instance instanceof LoopInterface); + return self::$instance->addPeriodicTimer($interval, $callback); } @@ -198,6 +208,8 @@ public static function futureTick($listener) self::get(); } + assert(self::$instance instanceof LoopInterface); + self::$instance->futureTick($listener); } @@ -216,6 +228,8 @@ public static function addSignal($signal, $listener) self::get(); } + assert(self::$instance instanceof LoopInterface); + self::$instance->addSignal($signal, $listener); } @@ -247,6 +261,8 @@ public static function run() self::get(); } + assert(self::$instance instanceof LoopInterface); + self::$instance->run(); } diff --git a/src/SignalsHandler.php b/src/SignalsHandler.php index 10d125df..3538fc8c 100644 --- a/src/SignalsHandler.php +++ b/src/SignalsHandler.php @@ -47,6 +47,9 @@ public function call($signal) } } + /** + * @phpstan-impure + */ public function count($signal) { if (!isset($this->signals[$signal])) { diff --git a/src/StreamSelectLoop.php b/src/StreamSelectLoop.php index 943a81aa..5933d1ad 100644 --- a/src/StreamSelectLoop.php +++ b/src/StreamSelectLoop.php @@ -63,6 +63,9 @@ final class StreamSelectLoop implements LoopInterface private $running; private $pcntl = false; private $pcntlPoll = false; + /** + * @var SignalsHandler + */ private $signals; public function __construct() @@ -183,7 +186,13 @@ public function run() $this->timers->tick(); - // Future-tick queue has pending callbacks ... + /** + * Future-tick queue has pending callbacks ... + * + * + * @link https://github.com/phpstan/phpstan/issues/10566 + * @phpstan-ignore-next-line + */ if (!$this->running || !$this->futureTickQueue->isEmpty()) { $timeout = 0; @@ -286,7 +295,7 @@ private function streamSelect(array &$read, array &$write, $timeout) } } - /** @var ?callable $previous */ + /** @var ?(callable(int, string, string, int): bool) $previous */ $previous = \set_error_handler(function ($errno, $errstr) use (&$previous) { // suppress warnings that occur when `stream_select()` is interrupted by a signal // PHP defines `EINTR` through `ext-sockets` or `ext-pcntl`, otherwise use common default (Linux & Mac) @@ -305,7 +314,7 @@ private function streamSelect(array &$read, array &$write, $timeout) } catch (\Throwable $e) { // @codeCoverageIgnoreStart \restore_error_handler(); throw $e; - } catch (\Exception $e) { + } catch (\Exception $e) { /** @phpstan-ignore-line */ \restore_error_handler(); throw $e; } // @codeCoverageIgnoreEnd diff --git a/tests/ExtEventLoopTest.php b/tests/ExtEventLoopTest.php index af4caa13..2d107642 100644 --- a/tests/ExtEventLoopTest.php +++ b/tests/ExtEventLoopTest.php @@ -62,15 +62,6 @@ public function createStream() return $stream; } - public function writeToStream($stream, $content) - { - if ('Linux' !== PHP_OS) { - return parent::writeToStream($stream, $content); - } - - fwrite($stream, $content); - } - /** * @group epoll-readable-error */ diff --git a/tests/StreamSelectLoopTest.php b/tests/StreamSelectLoopTest.php index 7e2435a8..d4cf072c 100644 --- a/tests/StreamSelectLoopTest.php +++ b/tests/StreamSelectLoopTest.php @@ -61,6 +61,7 @@ public function testStreamSelectReportsWarningForStreamWithFilter() $error = null; $previous = set_error_handler(function ($_, $errstr) use (&$error) { $error = $errstr; + return true; }); try { @@ -73,7 +74,9 @@ public function testStreamSelectReportsWarningForStreamWithFilter() $this->assertNotNull($error); - $now = set_error_handler(function () { }); + $now = set_error_handler(function () { + return true; + }); restore_error_handler(); $this->assertEquals($previous, $now); } @@ -114,7 +117,9 @@ public function testStreamSelectThrowsWhenCustomErrorHandlerThrowsForStreamWithF $this->assertInstanceOf('RuntimeException', $e); - $now = set_error_handler(function () { }); + $now = set_error_handler(function () { + return true; + }); restore_error_handler(); $this->assertEquals($previous, $now); } @@ -176,7 +181,6 @@ public function testSignalInterruptWithStream($signal) $loop = $this->loop; list($writeStream, $readStream) = $this->createSocketPair(); $loop->addReadStream($readStream, function ($stream) use ($loop) { - /** @var $loop LoopInterface */ $read = fgets($stream); if ($read === "end loop\n") { $loop->stop(); diff --git a/tests/bin/12-undefined.php b/tests/bin/12-undefined.php index c45cc0f4..339ea7f3 100644 --- a/tests/bin/12-undefined.php +++ b/tests/bin/12-undefined.php @@ -9,4 +9,9 @@ echo 'never'; }); +/** + * We're ignore this line because the test using this file relies on the error caused by it. + * + * @phpstan-ignore-next-line + */ $undefined->foo('bar'); diff --git a/tests/bin/22-stop-uncaught.php b/tests/bin/22-stop-uncaught.php index 5b6142ed..a2654f12 100644 --- a/tests/bin/22-stop-uncaught.php +++ b/tests/bin/22-stop-uncaught.php @@ -9,6 +9,11 @@ echo 'never'; }); +/** + * Ignoring the next line until we raise the minimum PHP version to 7.1 + * + * @phpstan-ignore-next-line + */ set_exception_handler(function (Exception $e) { echo 'Uncaught error occured' . PHP_EOL; Loop::stop();