diff --git a/composer.json b/composer.json index f53002b..366a5cd 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ "ext-json": "*", "phpunit/phpunit": "^9", "jetbrains/phpstorm-stubs": "^2019.3", - "psalm/phar": "6.20.*" + "psalm/phar": "^7.0.0-rc10" }, "autoload": { "psr-4": { diff --git a/examples/benchmark-memory.php b/examples/benchmark-memory.php index 491d3de..9210989 100644 --- a/examples/benchmark-memory.php +++ b/examples/benchmark-memory.php @@ -11,6 +11,10 @@ use Revolt\EventLoop; use Revolt\EventLoop\Driver; +use Revolt\EventLoop\Driver\EvDriver; +use Revolt\EventLoop\Driver\EventDriver; +use Revolt\EventLoop\Driver\StreamSelectDriver; +use Revolt\EventLoop\Driver\UvDriver; require __DIR__ . '/../vendor/autoload.php'; @@ -21,15 +25,17 @@ /** @psalm-suppress RiskyCast */ $t = (int) \round((\array_key_exists('t', $args) ? (int) $args['t'] : 0)); -if (\array_key_exists('d', $args)) { - if (\is_string($args['d'])) { - $loopClass = 'Revolt\EventLoop\Driver\\' . $args['d'] . 'Driver'; - if (\class_exists($loopClass)) { - $loop = new ($loopClass)(); - if ($loop instanceof Driver) { - EventLoop::setDriver($loop); - } - } +if (\array_key_exists('d', $args) && \is_string($args['d'])) { + $loop = match ($args['d']) { + 'Ev' => EvDriver::isSupported() ? new EvDriver() : null, + 'Event' => EventDriver::isSupported() ? new EventDriver() : null, + 'StreamSelect' => new StreamSelectDriver(), + 'Uv' => UvDriver::isSupported() ? new UvDriver() : null, + default => null, + }; + + if ($loop instanceof Driver) { + EventLoop::setDriver($loop); } } diff --git a/examples/fiber-local-automatic.php b/examples/fiber-local-automatic.php index 7a8d0c7..014efca 100644 --- a/examples/fiber-local-automatic.php +++ b/examples/fiber-local-automatic.php @@ -13,12 +13,17 @@ * This might be used to log the current URL, authenticated user, or request identifier in an HTTP server. * * @psalm-suppress UnusedProperty + * + * @psalm-mutable */ final class Logger { private int $nextId = 1; private FiberLocal $transactionId; + /** + * @psalm-capabilities read-props + */ public function __construct() { $this->transactionId = new FiberLocal(fn () => $this->nextId++); diff --git a/examples/fiber-local-manual.php b/examples/fiber-local-manual.php index 04bb7fd..5ffe25c 100644 --- a/examples/fiber-local-manual.php +++ b/examples/fiber-local-manual.php @@ -14,10 +14,13 @@ * * @psalm-suppress UnusedProperty */ -final class Logger +final class ManualLogger { private FiberLocal $transactionId; + /** + * @psalm-capabilities read-props + */ public function __construct() { $this->transactionId = new FiberLocal(fn () => throw new \Exception('Unknown transaction ID')); @@ -45,7 +48,7 @@ public function log(string $message): void } } -$logger = new Logger(); +$logger = new ManualLogger(); $logger->setTransactionId(1); EventLoop::delay(1, static function () use ($logger) { diff --git a/examples/http-client-suspension.php b/examples/http-client-suspension.php index c15a3af..bcc3965 100644 --- a/examples/http-client-suspension.php +++ b/examples/http-client-suspension.php @@ -57,9 +57,15 @@ function fetch(string $url): string return $buffer; } +/** + * Returns the header block of an HTTP response. + * + * @psalm-pure + */ function extractHeader(string $httpResponse): string { return \explode("\r\n\r\n", $httpResponse, 2)[0]; } +// Printed on the CLI, where the header is text rather than HTML. echo extractHeader(fetch('http://www.google.com/')); diff --git a/psalm.xml b/psalm.xml index fd49f97..9cee111 100644 --- a/psalm.xml +++ b/psalm.xml @@ -15,12 +15,6 @@ - - - - - - @@ -98,6 +92,19 @@ + + + + + + + + + + + + + diff --git a/src/EventLoop.php b/src/EventLoop.php index a6b1fa1..6868b50 100644 --- a/src/EventLoop.php +++ b/src/EventLoop.php @@ -16,6 +16,9 @@ /** * Accessor to allow global access to the event loop. * + * Scheduling and suspension are `io` for callers, like `usleep()`: the driver is the scheduler, and + * what other fibers do while the current one is suspended is their own effect. + * * @see Driver */ final class EventLoop @@ -35,24 +38,36 @@ public static function setDriver(Driver $driver): void try { /** @psalm-suppress InternalClass */ self::$driver = new class () extends AbstractDriver { + /** + * @psalm-pure + */ #[\Override] protected function activate(array $callbacks): void { throw new \Error("Can't activate callback during garbage collection."); } + /** + * @psalm-pure + */ #[\Override] protected function dispatch(bool $blocking): void { throw new \Error("Can't dispatch during garbage collection."); } + /** + * @psalm-capabilities read-props + */ #[\Override] protected function deactivate(DriverCallback $callback): void { // do nothing } + /** + * @psalm-pure + */ #[\Override] public function getHandle(): mixed { @@ -83,6 +98,9 @@ protected function now(): float * * @param \Closure(...):void $closure The callback to queue. * @param mixed ...$args The callback arguments. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function queue(\Closure $closure, mixed ...$args): void { @@ -102,6 +120,9 @@ public static function queue(\Closure $closure, mixed ...$args): void * invalidated before the callback invocation. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function defer(\Closure $closure): string { @@ -122,6 +143,9 @@ public static function defer(\Closure $closure): string * before the callback invocation. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function delay(float $delay, \Closure $closure): string { @@ -142,6 +166,9 @@ public static function delay(float $delay, \Closure $closure): string * @param \Closure(string):void $closure The callback to repeat. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function repeat(float $interval, \Closure $closure): string { @@ -165,6 +192,9 @@ public static function repeat(float $interval, \Closure $closure): string * @param \Closure(string, resource):void $closure The callback to execute. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function onReadable(mixed $stream, \Closure $closure): string { @@ -188,6 +218,9 @@ public static function onReadable(mixed $stream, \Closure $closure): string * @param \Closure(string, resource):void $closure The callback to execute. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function onWritable(mixed $stream, \Closure $closure): string { @@ -212,6 +245,9 @@ public static function onWritable(mixed $stream, \Closure $closure): string * @return string A unique identifier that can be used to cancel, enable or disable the callback. * * @throws UnsupportedFeatureException If signal handling is not supported. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function onSignal(int $signal, \Closure $closure): string { @@ -229,6 +265,9 @@ public static function onSignal(int $signal, \Closure $closure): string * @return string The callback identifier. * * @throws InvalidCallbackError If the callback identifier is invalid. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function enable(string $callbackId): string { @@ -247,6 +286,9 @@ public static function enable(string $callbackId): string * @param string $callbackId The callback identifier. * * @return string The callback identifier. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function disable(string $callbackId): string { @@ -260,6 +302,9 @@ public static function disable(string $callbackId): string * callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier. * * @param string $callbackId The callback identifier. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function cancel(string $callbackId): void { @@ -277,6 +322,9 @@ public static function cancel(string $callbackId): void * @return string The callback identifier. * * @throws InvalidCallbackError If the callback identifier is invalid. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function reference(string $callbackId): string { @@ -292,6 +340,9 @@ public static function reference(string $callbackId): string * @param string $callbackId The callback identifier. * * @return string The callback identifier. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function unreference(string $callbackId): string { @@ -352,6 +403,9 @@ public static function getType(string $callbackId): CallbackType * @param string $callbackId The callback identifier. * * @return bool `true` if the callback is currently enabled, otherwise `false`. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function isEnabled(string $callbackId): bool { @@ -364,6 +418,9 @@ public static function isEnabled(string $callbackId): bool * @param string $callbackId The callback identifier. * * @return bool `true` if the callback is currently referenced, otherwise `false`. + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function isReferenced(string $callbackId): bool { @@ -391,6 +448,9 @@ public static function getDriver(): Driver * Calls from the same fiber will return the same suspension object. * * @return Suspension + * + * @psalm-capabilities io + * @psalm-suppress ImpureMethodCall */ public static function getSuspension(): Suspension { @@ -415,6 +475,8 @@ public static function run(): void * Disable construction as this is a static class. * * @psalm-suppress UnusedConstructor + * + * @psalm-capabilities read-props */ private function __construct() { diff --git a/src/EventLoop/Driver.php b/src/EventLoop/Driver.php index 80464aa..ceea32c 100644 --- a/src/EventLoop/Driver.php +++ b/src/EventLoop/Driver.php @@ -7,6 +7,8 @@ /** * The driver MUST run in its own fiber and execute callbacks in a separate fiber. If fibers are reused, the driver * needs to call {@see FiberLocal::clear()} after running the callback. + * + * @psalm-mutable */ interface Driver { @@ -24,6 +26,8 @@ interface Driver * error handler or exceptions that would be passed to an error handler but none exists to handle them. * * @throws \Error Thrown if the event loop is already running. + * + * @psalm-impure */ public function run(): void; @@ -32,6 +36,8 @@ public function run(): void; * * When an event loop is stopped, it continues with its current tick and exits the loop afterwards. Multiple calls * to stop MUST be ignored and MUST NOT raise an exception. + * + * @psalm-impure */ public function stop(): void; @@ -41,11 +47,15 @@ public function stop(): void; * Calls from the same fiber will return the same suspension object. * * @return Suspension + * + * @psalm-impure */ public function getSuspension(): Suspension; /** * @return bool True if the event loop is running, false if it is stopped. + * + * @psalm-impure */ public function isRunning(): bool; @@ -60,6 +70,8 @@ public function isRunning(): bool; * * @param \Closure(...):void $closure The callback to queue. * @param mixed ...$args The callback arguments. + * + * @psalm-impure */ public function queue(\Closure $closure, mixed ...$args): void; @@ -76,6 +88,8 @@ public function queue(\Closure $closure, mixed ...$args): void; * callback invocation. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-impure */ public function defer(\Closure $closure): string; @@ -93,6 +107,8 @@ public function defer(\Closure $closure): string; * callback invocation. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-impure */ public function delay(float $delay, \Closure $closure): string; @@ -110,6 +126,8 @@ public function delay(float $delay, \Closure $closure): string; * @param \Closure(string):void $closure The callback to repeat. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-impure */ public function repeat(float $interval, \Closure $closure): string; @@ -130,6 +148,8 @@ public function repeat(float $interval, \Closure $closure): string; * @param \Closure(string, resource):void $closure The callback to execute. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-impure */ public function onReadable(mixed $stream, \Closure $closure): string; @@ -150,6 +170,8 @@ public function onReadable(mixed $stream, \Closure $closure): string; * @param \Closure(string, resource):void $closure The callback to execute. * * @return string A unique identifier that can be used to cancel, enable or disable the callback. + * + * @psalm-impure */ public function onWritable(mixed $stream, \Closure $closure): string; @@ -171,6 +193,8 @@ public function onWritable(mixed $stream, \Closure $closure): string; * @return string A unique identifier that can be used to cancel, enable or disable the callback. * * @throws UnsupportedFeatureException If signal handling is not supported. + * + * @psalm-impure */ public function onSignal(int $signal, \Closure $closure): string; @@ -185,6 +209,8 @@ public function onSignal(int $signal, \Closure $closure): string; * @return string The callback identifier. * * @throws InvalidCallbackError If the callback identifier is invalid. + * + * @psalm-impure */ public function enable(string $callbackId): string; @@ -195,6 +221,8 @@ public function enable(string $callbackId): string; * callback is permanently invalid. Calling this function MUST NOT fail, even if passed an invalid identifier. * * @param string $callbackId The callback identifier. + * + * @psalm-impure */ public function cancel(string $callbackId): void; @@ -210,6 +238,8 @@ public function cancel(string $callbackId): void; * @param string $callbackId The callback identifier. * * @return string The callback identifier. + * + * @psalm-impure */ public function disable(string $callbackId): string; @@ -224,6 +254,8 @@ public function disable(string $callbackId): string; * @return string The callback identifier. * * @throws InvalidCallbackError If the callback identifier is invalid. + * + * @psalm-impure */ public function reference(string $callbackId): string; @@ -236,6 +268,8 @@ public function reference(string $callbackId): string; * @param string $callbackId The callback identifier. * * @return string The callback identifier. + * + * @psalm-impure */ public function unreference(string $callbackId): string; @@ -250,6 +284,8 @@ public function unreference(string $callbackId): string; * * @param null|\Closure(\Throwable):void $errorHandler The callback to execute. `null` will clear the current * handler. + * + * @psalm-impure */ public function setErrorHandler(?\Closure $errorHandler): void; @@ -257,6 +293,8 @@ public function setErrorHandler(?\Closure $errorHandler): void; * Gets the error handler closure or {@code null} if none is set. * * @return null|\Closure(\Throwable):void The previous handler, `null` if there was none. + * + * @psalm-impure */ public function getErrorHandler(): ?\Closure; @@ -270,6 +308,8 @@ public function getErrorHandler(): ?\Closure; * instance. * * @return null|object|resource The loop handle the event loop operates on. `null` if there is none. + * + * @psalm-impure */ public function getHandle(): mixed; @@ -277,6 +317,8 @@ public function getHandle(): mixed; * Returns all registered non-cancelled callback identifiers. * * @return string[] Callback identifiers. + * + * @psalm-impure */ public function getIdentifiers(): array; @@ -286,6 +328,8 @@ public function getIdentifiers(): array; * @param string $callbackId The callback identifier. * * @return CallbackType The callback type. + * + * @psalm-impure */ public function getType(string $callbackId): CallbackType; @@ -295,6 +339,8 @@ public function getType(string $callbackId): CallbackType; * @param string $callbackId The callback identifier. * * @return bool `true` if the callback is currently enabled, otherwise `false`. + * + * @psalm-impure */ public function isEnabled(string $callbackId): bool; @@ -304,6 +350,8 @@ public function isEnabled(string $callbackId): bool; * @param string $callbackId The callback identifier. * * @return bool `true` if the callback is currently referenced, otherwise `false`. + * + * @psalm-impure */ public function isReferenced(string $callbackId): bool; @@ -313,6 +361,8 @@ public function isReferenced(string $callbackId): bool; * If this method isn't implemented, dumping the event loop in a busy application, even indirectly, is a pain. * * @return array + * + * @psalm-impure */ public function __debugInfo(): array; } diff --git a/src/EventLoop/Driver/EvDriver.php b/src/EventLoop/Driver/EvDriver.php index 9699664..60caa25 100644 --- a/src/EventLoop/Driver/EvDriver.php +++ b/src/EventLoop/Driver/EvDriver.php @@ -19,6 +19,9 @@ final class EvDriver extends AbstractDriver /** @var array|null */ private static ?array $activeSignals = null; + /** + * @psalm-pure + */ public static function isSupported(): bool { return \extension_loaded("ev"); diff --git a/src/EventLoop/Driver/EventDriver.php b/src/EventLoop/Driver/EventDriver.php index 36b6040..02e0a50 100644 --- a/src/EventLoop/Driver/EventDriver.php +++ b/src/EventLoop/Driver/EventDriver.php @@ -19,6 +19,9 @@ final class EventDriver extends AbstractDriver /** @var array|null */ private static ?array $activeSignals = null; + /** + * @psalm-pure + */ public static function isSupported(): bool { return \extension_loaded("event"); diff --git a/src/EventLoop/Driver/StreamSelectDriver.php b/src/EventLoop/Driver/StreamSelectDriver.php index d072ff3..1f634cd 100644 --- a/src/EventLoop/Driver/StreamSelectDriver.php +++ b/src/EventLoop/Driver/StreamSelectDriver.php @@ -53,7 +53,10 @@ public function __construct() && \function_exists('pcntl_signal_dispatch') && \function_exists('pcntl_signal'); - $this->streamSelectErrorHandler = function (int $errno, string $message): void { + $this->streamSelectErrorHandler = /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ + function (int $errno, string $message): void { // Casing changed in PHP 8 from 'unable' to 'Unable' if (\stripos($message, "stream_select(): unable to select [4]: ") === 0) { // EINTR $this->streamSelectIgnoreResult = true; @@ -102,6 +105,9 @@ public function onSignal(int $signal, \Closure $closure): string return parent::onSignal($signal, $closure); } + /** + * @psalm-pure + */ #[\Override] public function getHandle(): mixed { diff --git a/src/EventLoop/Driver/TracingDriver.php b/src/EventLoop/Driver/TracingDriver.php index 1888e99..4951f63 100644 --- a/src/EventLoop/Driver/TracingDriver.php +++ b/src/EventLoop/Driver/TracingDriver.php @@ -207,6 +207,9 @@ public function getHandle(): mixed return $this->driver->getHandle(); } + /** + * @psalm-capabilities read-props + */ public function dump(): string { $dump = "Enabled, referenced callbacks keeping the loop running: "; @@ -260,11 +263,17 @@ public function queue(\Closure $closure, mixed ...$args): void $this->driver->queue($closure, ...$args); } + /** + * @psalm-capabilities read-props + */ private function getCreationTrace(string $callbackId): string { return $this->creationTraces[$callbackId] ?? 'No creation trace, yet.'; } + /** + * @psalm-capabilities read-props + */ private function getCancelTrace(string $callbackId): string { return $this->cancelTraces[$callbackId] ?? 'No cancellation trace, yet.'; @@ -284,6 +293,8 @@ private function getCancelTrace(string $callbackId): string * }> $trace Output of `debug_backtrace()`. * * @return string Formatted stacktrace. + * + * @psalm-pure */ private function formatStacktrace(array $trace): string { diff --git a/src/EventLoop/Driver/UvDriver.php b/src/EventLoop/Driver/UvDriver.php index 98c81dd..d213fba 100644 --- a/src/EventLoop/Driver/UvDriver.php +++ b/src/EventLoop/Driver/UvDriver.php @@ -14,6 +14,9 @@ final class UvDriver extends AbstractDriver { + /** + * @psalm-pure + */ public static function isSupported(): bool { return \extension_loaded("uv"); @@ -129,6 +132,9 @@ public function getHandle(): mixed return $this->handle; } + /** + * @psalm-capabilities read-props + */ #[\Override] protected function now(): float { @@ -138,6 +144,9 @@ protected function now(): float return \uv_now($this->handle) / 1000; } + /** + * @psalm-capabilities read-props + */ #[\Override] protected function dispatch(bool $blocking): void { @@ -145,6 +154,9 @@ protected function dispatch(bool $blocking): void \uv_run($this->handle, $blocking ? \UV::RUN_ONCE : \UV::RUN_NOWAIT); } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ #[\Override] protected function activate(array $callbacks): void { @@ -213,6 +225,9 @@ protected function activate(array $callbacks): void } } + /** + * @psalm-capabilities read-props + */ #[\Override] protected function deactivate(DriverCallback $callback): void { @@ -252,6 +267,9 @@ protected function deactivate(DriverCallback $callback): void } } + /** + * @psalm-pure + */ private function getStreamCallbackFlags(StreamCallback $callback): int { if ($callback instanceof StreamWritableCallback) { diff --git a/src/EventLoop/FiberLocal.php b/src/EventLoop/FiberLocal.php index 34dd261..395d87f 100644 --- a/src/EventLoop/FiberLocal.php +++ b/src/EventLoop/FiberLocal.php @@ -10,6 +10,8 @@ * Each instance stores data separately for each fiber. Usage examples include contextual logging data. * * @template T + * + * @api */ final class FiberLocal { diff --git a/src/EventLoop/Internal/AbstractDriver.php b/src/EventLoop/Internal/AbstractDriver.php index ef7513b..8e6236b 100644 --- a/src/EventLoop/Internal/AbstractDriver.php +++ b/src/EventLoop/Internal/AbstractDriver.php @@ -131,6 +131,9 @@ public function stop(): void $this->stopped = true; } + /** + * @psalm-capabilities read-props + */ #[\Override] public function isRunning(): bool { @@ -143,6 +146,9 @@ public function queue(\Closure $closure, mixed ...$args): void $this->microtaskQueue->enqueue([$closure, $args]); } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ #[\Override] public function defer(\Closure $closure): string { @@ -184,6 +190,9 @@ public function repeat(float $interval, \Closure $closure): string return $timerCallback->id; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ #[\Override] public function onReadable(mixed $stream, \Closure $closure): string { @@ -195,6 +204,9 @@ public function onReadable(mixed $stream, \Closure $closure): string return $streamCallback->id; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ #[\Override] public function onWritable($stream, \Closure $closure): string { @@ -333,6 +345,9 @@ public function getSuspension(): Suspension return $suspension; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ #[\Override] public function setErrorHandler(?\Closure $errorHandler): void { @@ -345,6 +360,9 @@ public function getErrorHandler(): ?\Closure return $this->errorHandler; } + /** + * @psalm-capabilities read-props + */ #[\Override] public function __debugInfo(): array { @@ -357,12 +375,18 @@ public function __debugInfo(): array // @codeCoverageIgnoreEnd } + /** + * @psalm-capabilities read-props + */ #[\Override] public function getIdentifiers(): array { return \array_keys($this->callbacks); } + /** + * @psalm-capabilities read-props + */ #[\Override] public function getType(string $callbackId): CallbackType { @@ -371,6 +395,9 @@ public function getType(string $callbackId): CallbackType return $callback->getType(); } + /** + * @psalm-capabilities read-props + */ #[\Override] public function isEnabled(string $callbackId): bool { @@ -379,6 +406,9 @@ public function isEnabled(string $callbackId): bool return $callback->enabled; } + /** + * @psalm-capabilities read-props + */ #[\Override] public function isReferenced(string $callbackId): bool { @@ -391,16 +421,22 @@ public function isReferenced(string $callbackId): bool * Activates (enables) all the given callbacks. * * @param array $callbacks + * + * @psalm-impure */ abstract protected function activate(array $callbacks): void; /** * Dispatches any pending read/write, timer, and signal events. + * + * @psalm-impure */ abstract protected function dispatch(bool $blocking): void; /** * Deactivates (disables) the given callback. + * + * @psalm-impure */ abstract protected function deactivate(DriverCallback $callback): void; @@ -436,6 +472,8 @@ final protected function error(\Closure $closure, \Throwable $exception): void * * Note this value does not necessarily correlate to wall-clock time, rather the value returned is meant to be used * in relative comparisons to prior values returned by this method (intervals, expiration calculations, etc.). + * + * @psalm-impure */ abstract protected function now(): float; @@ -464,6 +502,8 @@ private function invokeMicrotasks(): void /** * @return bool True if no enabled and referenced callbacks remain in the loop. + * + * @psalm-capabilities read-props */ private function isEmpty(): bool { @@ -538,6 +578,8 @@ private function invokeCallbacks(): void /** * @param \Closure():mixed $interrupt + * + * @psalm-capabilities read-props|write-this-props|write-refs */ private function setInterrupt(\Closure $interrupt): void { @@ -546,6 +588,9 @@ private function setInterrupt(\Closure $interrupt): void $this->interrupt = $interrupt; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ private function invokeInterrupt(): void { \assert($this->interrupt !== null); @@ -663,6 +708,9 @@ private function createErrorCallback(): void }; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ private function callbackId(): string { $callbackId = $this->nextId; @@ -677,6 +725,9 @@ private function callbackId(): string return $callbackId; } + /** + * @psalm-pure + */ final public function __serialize(): never { throw new \Error(__CLASS__ . ' does not support serialization'); @@ -684,6 +735,8 @@ final public function __serialize(): never /** * @psalm-suppress UnusedParam $data param required. + * + * @psalm-pure */ final public function __unserialize(array $data): never { diff --git a/src/EventLoop/Internal/ClosureHelper.php b/src/EventLoop/Internal/ClosureHelper.php index 48f8fa2..da7478b 100644 --- a/src/EventLoop/Internal/ClosureHelper.php +++ b/src/EventLoop/Internal/ClosureHelper.php @@ -4,9 +4,16 @@ namespace Revolt\EventLoop\Internal; -/** @internal */ +/** + * @internal + * + * @psalm-immutable + */ final class ClosureHelper { + /** + * @psalm-capabilities read-props + */ public static function getDescription(\Closure $closure): string { try { diff --git a/src/EventLoop/Internal/DeferCallback.php b/src/EventLoop/Internal/DeferCallback.php index b2bd843..21041ae 100644 --- a/src/EventLoop/Internal/DeferCallback.php +++ b/src/EventLoop/Internal/DeferCallback.php @@ -9,6 +9,9 @@ /** @internal */ final class DeferCallback extends DriverCallback { + /** + * @psalm-pure + */ #[\Override] public function getType(): CallbackType { diff --git a/src/EventLoop/Internal/DriverCallback.php b/src/EventLoop/Internal/DriverCallback.php index 8cb47bf..8e8e492 100644 --- a/src/EventLoop/Internal/DriverCallback.php +++ b/src/EventLoop/Internal/DriverCallback.php @@ -23,10 +23,15 @@ public function __construct( ) { } + /** + * @psalm-capabilities read-props + */ abstract public function getType(): CallbackType; /** * @param string $property + * + * @psalm-pure */ public function __get(string $property): never { @@ -36,6 +41,8 @@ public function __get(string $property): never /** * @param string $property * @param mixed $value + * + * @psalm-pure */ public function __set(string $property, mixed $value): never { diff --git a/src/EventLoop/Internal/DriverSuspension.php b/src/EventLoop/Internal/DriverSuspension.php index e0edbc7..1b940e3 100644 --- a/src/EventLoop/Internal/DriverSuspension.php +++ b/src/EventLoop/Internal/DriverSuspension.php @@ -13,6 +13,8 @@ * * @template T * @implements Suspension + * + * @psalm-suppress ImmutableDependency The driver's bookkeeping and the fibers it runs are not the caller's effects. */ final class DriverSuspension implements Suspension { @@ -179,6 +181,9 @@ public function throw(\Throwable $throwable): void } } + /** + * @psalm-pure + */ private function formatStacktrace(array $trace): string { return \implode("\n", \array_map(static function ($e, $i) { diff --git a/src/EventLoop/Internal/SignalCallback.php b/src/EventLoop/Internal/SignalCallback.php index 33e02cf..5110cd9 100644 --- a/src/EventLoop/Internal/SignalCallback.php +++ b/src/EventLoop/Internal/SignalCallback.php @@ -9,6 +9,9 @@ /** @internal */ final class SignalCallback extends DriverCallback { + /** + * @psalm-capabilities read-props + */ public function __construct( string $id, \Closure $closure, @@ -17,6 +20,9 @@ public function __construct( parent::__construct($id, $closure); } + /** + * @psalm-pure + */ #[\Override] public function getType(): CallbackType { diff --git a/src/EventLoop/Internal/StreamCallback.php b/src/EventLoop/Internal/StreamCallback.php index 56a2547..292d20f 100644 --- a/src/EventLoop/Internal/StreamCallback.php +++ b/src/EventLoop/Internal/StreamCallback.php @@ -9,6 +9,8 @@ abstract class StreamCallback extends DriverCallback { /** * @param resource $stream + * + * @psalm-capabilities read-props */ public function __construct( string $id, diff --git a/src/EventLoop/Internal/StreamReadableCallback.php b/src/EventLoop/Internal/StreamReadableCallback.php index a68c793..39babee 100644 --- a/src/EventLoop/Internal/StreamReadableCallback.php +++ b/src/EventLoop/Internal/StreamReadableCallback.php @@ -9,6 +9,9 @@ /** @internal */ final class StreamReadableCallback extends StreamCallback { + /** + * @psalm-pure + */ #[\Override] public function getType(): CallbackType { diff --git a/src/EventLoop/Internal/StreamWritableCallback.php b/src/EventLoop/Internal/StreamWritableCallback.php index a3c8626..23c3df8 100644 --- a/src/EventLoop/Internal/StreamWritableCallback.php +++ b/src/EventLoop/Internal/StreamWritableCallback.php @@ -9,6 +9,9 @@ /** @internal */ final class StreamWritableCallback extends StreamCallback { + /** + * @psalm-pure + */ #[\Override] public function getType(): CallbackType { diff --git a/src/EventLoop/Internal/TimerCallback.php b/src/EventLoop/Internal/TimerCallback.php index 7b6d314..1fcdf5c 100644 --- a/src/EventLoop/Internal/TimerCallback.php +++ b/src/EventLoop/Internal/TimerCallback.php @@ -9,6 +9,9 @@ /** @internal */ final class TimerCallback extends DriverCallback { + /** + * @psalm-capabilities read-props + */ public function __construct( string $id, public readonly float $interval, @@ -19,6 +22,9 @@ public function __construct( parent::__construct($id, $callback); } + /** + * @psalm-capabilities read-props + */ #[\Override] public function getType(): CallbackType { diff --git a/src/EventLoop/Internal/TimerQueue.php b/src/EventLoop/Internal/TimerQueue.php index 8caef94..b070533 100644 --- a/src/EventLoop/Internal/TimerQueue.php +++ b/src/EventLoop/Internal/TimerQueue.php @@ -8,6 +8,8 @@ * Uses a binary tree stored in an array to implement a heap. * * @internal + * + * @psalm-capabilities read-props|write-this-props|write-refs */ final class TimerQueue { @@ -21,6 +23,8 @@ final class TimerQueue * Inserts the callback into the queue. * * Time complexity: O(log(n)). + * + * @psalm-capabilities read-props|write-this-props|write-refs */ public function insert(TimerCallback $callback): void { @@ -37,6 +41,8 @@ public function insert(TimerCallback $callback): void * Removes the given callback from the queue. * * Time complexity: O(log(n)). + * + * @psalm-capabilities read-props|write-this-props|write-refs */ public function remove(TimerCallback $callback): void { @@ -57,6 +63,8 @@ public function remove(TimerCallback $callback): void * @param float $now Current event loop time. * * @return TimerCallback|null Expired callback at the top of the heap or null if the callback has not expired. + * + * @psalm-capabilities read-props|write-this-props|write-refs */ public function extract(float $now): ?TimerCallback { @@ -80,6 +88,8 @@ public function extract(float $now): ?TimerCallback * Time complexity: O(1). * * @return float|null Expiration time of the callback at the top of the heap or null if the heap is empty. + * + * @psalm-capabilities read-props */ public function peek(): ?float { @@ -88,6 +98,8 @@ public function peek(): ?float /** * @param int $node Rebuild the data array from the given node upward. + * + * @psalm-capabilities read-props|write-this-props|write-refs */ private function heapifyUp(int $node): void { @@ -100,6 +112,8 @@ private function heapifyUp(int $node): void /** * @param int $node Rebuild the data array from the given node downward. + * + * @psalm-capabilities read-props|write-this-props|write-refs */ private function heapifyDown(int $node): void { @@ -122,6 +136,9 @@ private function heapifyDown(int $node): void } } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ private function swap(int $left, int $right): void { $temp = $this->callbacks[$left]; @@ -135,6 +152,8 @@ private function swap(int $left, int $right): void /** * @param int $node Remove the given node and then rebuild the data array. + * + * @psalm-capabilities read-props|write-this-props|write-refs */ private function removeAndRebuild(int $node): void { diff --git a/src/EventLoop/InvalidCallbackError.php b/src/EventLoop/InvalidCallbackError.php index 8be00e2..f1ea0e2 100644 --- a/src/EventLoop/InvalidCallbackError.php +++ b/src/EventLoop/InvalidCallbackError.php @@ -6,6 +6,11 @@ use Revolt\EventLoop\Internal\ClosureHelper; +/** + * Error thrown for an invalid event-loop callback. + * + * @api + */ final class InvalidCallbackError extends \Error { public const E_NONNULL_RETURN = 1; @@ -13,6 +18,8 @@ final class InvalidCallbackError extends \Error /** * MUST be thrown if any callback returns a non-null value. + * + * @psalm-capabilities read-props */ public static function nonNullReturn(string $callbackId, \Closure $closure): self { @@ -27,6 +34,8 @@ public static function nonNullReturn(string $callbackId, \Closure $closure): sel * MUST be thrown if any operation (except disable() and cancel()) is attempted with an invalid callback identifier. * * An invalid callback identifier is any identifier that is not yet emitted by the driver or cancelled by the user. + * + * @psalm-pure */ public static function invalidIdentifier(string $callbackId): self { @@ -45,6 +54,8 @@ public static function invalidIdentifier(string $callbackId): self /** * @param string $callbackId The callback identifier. * @param string $message The exception message. + * + * @psalm-capabilities read-props */ private function __construct(string $callbackId, int $code, string $message) { @@ -62,6 +73,9 @@ public function getCallbackId(): string return $this->callbackId; } + /** + * @psalm-capabilities read-props|write-this-props|write-refs + */ public function addInfo(string $key, string $message): void { $this->info[$key] = $message; diff --git a/src/EventLoop/Suspension.php b/src/EventLoop/Suspension.php index 216cfc1..5f1886b 100644 --- a/src/EventLoop/Suspension.php +++ b/src/EventLoop/Suspension.php @@ -21,11 +21,15 @@ * ``` * * @template T + * + * @psalm-mutable */ interface Suspension { /** * @param T $value The value to return from the call to {@see suspend()}. + * + * @psalm-capabilities io */ public function resume(mixed $value = null): void; @@ -33,11 +37,15 @@ public function resume(mixed $value = null): void; * Returns the value provided to {@see resume()} or throws the exception provided to {@see throw()}. * * @return T + * + * @psalm-capabilities io */ public function suspend(): mixed; /** * Throws the given exception from the call to {@see suspend()}. + * + * @psalm-capabilities io */ public function throw(\Throwable $throwable): void; } diff --git a/src/EventLoop/UncaughtThrowable.php b/src/EventLoop/UncaughtThrowable.php index 2ee4c74..9f9886a 100644 --- a/src/EventLoop/UncaughtThrowable.php +++ b/src/EventLoop/UncaughtThrowable.php @@ -8,6 +8,9 @@ final class UncaughtThrowable extends \Error { + /** + * @psalm-pure + */ public static function throwingCallback(\Closure $closure, \Throwable $previous): self { return new self( @@ -17,11 +20,17 @@ public static function throwingCallback(\Closure $closure, \Throwable $previous) ); } + /** + * @psalm-pure + */ public static function throwingErrorHandler(\Closure $closure, \Throwable $previous): self { return new self("Uncaught %s thrown in event loop error handler %s%s", $closure, $previous); } + /** + * @psalm-capabilities read-props + */ private function __construct(string $message, \Closure $closure, \Throwable $previous) { parent::__construct(\sprintf(