diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bde850f..c80ccdcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ jobs: name: PHPUnit (PHP ${{ matrix.php }}) runs-on: ubuntu-24.04 strategy: + fail-fast: false matrix: php: - 8.5 @@ -43,6 +44,7 @@ jobs: runs-on: ubuntu-24.04 continue-on-error: true strategy: + fail-fast: false matrix: php: - 8.5 @@ -79,6 +81,7 @@ jobs: runs-on: windows-2022 continue-on-error: true strategy: + fail-fast: false matrix: php: - 8.5 @@ -104,3 +107,39 @@ 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: + fail-fast: false + matrix: + php: + - 8.5 + - 8.4 + - 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 6d31e81d..915d7f34 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "php": ">=7.1" }, "require-dev": { + "phpstan/phpstan": "^1", "phpunit/phpunit": "^9.6 || ^7.5" }, "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 a069dd18..1c4a8be4 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; @@ -190,6 +190,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 @@ -197,6 +201,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) { @@ -220,11 +228,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 d18fb16c..477ddf55 100644 --- a/src/ExtEventLoop.php +++ b/src/ExtEventLoop.php @@ -192,6 +192,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 fd52c354..9115e5fd 100644 --- a/src/ExtUvLoop.php +++ b/src/ExtUvLoop.php @@ -210,6 +210,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 @@ -220,12 +224,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); } } @@ -258,6 +270,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/SignalsHandler.php b/src/SignalsHandler.php index e9b245ea..756055bd 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 41dd2cb3..1e52ecbe 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) diff --git a/tests/ExtEventLoopTest.php b/tests/ExtEventLoopTest.php index ce40ba58..2d107642 100644 --- a/tests/ExtEventLoopTest.php +++ b/tests/ExtEventLoopTest.php @@ -62,12 +62,28 @@ public function createStream() return $stream; } - public function writeToStream($stream, $content) + /** + * @group epoll-readable-error + */ + public function testCanUseReadableStreamWithFeatureFds() { - if ('Linux' !== PHP_OS) { - return parent::writeToStream($stream, $content); + if (PHP_VERSION_ID > 70000) { + $this->markTestSkipped('Memory stream not supported'); } - fwrite($stream, $content); + $this->loop = $this->createLoop(true); + + $input = fopen('php://temp/maxmemory:0', 'r+'); + + fwrite($input, 'x'); + ftruncate($input, 0); + + $this->loop->addReadStream($input, $this->expectCallableExactly(2)); + + fwrite($input, "foo\n"); + $this->tickLoop($this->loop); + + fwrite($input, "bar\n"); + $this->tickLoop($this->loop); } } diff --git a/tests/StreamSelectLoopTest.php b/tests/StreamSelectLoopTest.php index b2672d4e..4ebeb1c9 100644 --- a/tests/StreamSelectLoopTest.php +++ b/tests/StreamSelectLoopTest.php @@ -56,6 +56,7 @@ public function testStreamSelectReportsWarningForStreamWithFilter() $error = null; $previous = set_error_handler(function ($_, $errstr) use (&$error) { $error = $errstr; + return true; }); try { @@ -68,7 +69,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); } @@ -104,7 +107,9 @@ public function testStreamSelectThrowsWhenCustomErrorHandlerThrowsForStreamWithF $this->assertInstanceOf(\RuntimeException::class, $e); - $now = set_error_handler(function () { }); + $now = set_error_handler(function () { + return true; + }); restore_error_handler(); $this->assertEquals($previous, $now); } @@ -162,7 +167,6 @@ public function testSignalInterruptWithStream($signal) // add stream to the loop list($writeStream, $readStream) = $this->createSocketPair(); $this->loop->addReadStream($readStream, function ($stream) { - /** @var $loop LoopInterface */ $read = fgets($stream); if ($read === "end loop\n") { $this->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();