1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Update Amp usage

Fixed a few errors and used byte-stream for reading and writing.
This commit is contained in:
Aaron Piotrowski 2019-02-05 19:00:13 -06:00 committed by Matthew Brown
parent 263a4c8cf1
commit b0d97843ce
10 changed files with 267 additions and 404 deletions

View File

@ -22,7 +22,8 @@
"webmozart/glob": "^4.1", "webmozart/glob": "^4.1",
"webmozart/path-util": "^2.3", "webmozart/path-util": "^2.3",
"symfony/console": "^3.0||^4.0", "symfony/console": "^3.0||^4.0",
"amphp/amp": "^2.1" "amphp/amp": "^2.1",
"amphp/byte-stream": "^1.5"
}, },
"bin": ["psalm", "psalter", "psalm-language-server", "psalm-plugin"], "bin": ["psalm", "psalter", "psalm-language-server", "psalm-plugin"],
"autoload": { "autoload": {

View File

@ -36,7 +36,7 @@
</ignoreExceptions> </ignoreExceptions>
<stubs> <stubs>
<file name="src/Psalm/Internal/Stubs/SabreEvent.php"/> <file name="src/Psalm/Internal/Stubs/Amp.php"/>
</stubs> </stubs>
<plugins> <plugins>

View File

@ -7,6 +7,7 @@ use Psalm\Internal\LanguageServer\ClientHandler;
use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier}; use LanguageServerProtocol\{Diagnostic, TextDocumentItem, TextDocumentIdentifier};
use Amp\Promise; use Amp\Promise;
use JsonMapper; use JsonMapper;
use function Amp\call;
/** /**
* Provides method handlers for all textDocument/* methods * Provides method handlers for all textDocument/* methods
@ -53,21 +54,15 @@ class TextDocument
*/ */
public function xcontent(TextDocumentIdentifier $textDocument): Promise public function xcontent(TextDocumentIdentifier $textDocument): Promise
{ {
$promise = $this->handler->request( return call(
'textDocument/xcontent', function () use ($textDocument) {
['textDocument' => $textDocument] $result = yield $this->handler->request(
); 'textDocument/xcontent',
['textDocument' => $textDocument]
);
$promise->onResolve(
/**
* @param object $result
* @return object
*/
function ($result) {
return $this->mapper->map($result, new TextDocumentItem); return $this->mapper->map($result, new TextDocumentItem);
} }
); );
return $promise;
} }
} }

View File

@ -4,7 +4,9 @@ declare(strict_types = 1);
namespace Psalm\Internal\LanguageServer; namespace Psalm\Internal\LanguageServer;
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Amp\Deferred;
use Amp\Promise; use Amp\Promise;
use function Amp\call;
/** /**
* @internal * @internal
@ -38,54 +40,48 @@ class ClientHandler
* *
* @param string $method The method to call * @param string $method The method to call
* @param array|object $params The method parameters * @param array|object $params The method parameters
* @return Promise Resolved with the result of the request or rejected with an error * @return Promise <mixed> Resolved with the result of the request or rejected with an error
*/ */
public function request(string $method, $params): Promise public function request(string $method, $params): Promise
{ {
$id = $this->idGenerator->generate(); $id = $this->idGenerator->generate();
$promise = $this->protocolWriter->write( return call(function () use ($id, $method, $params) {
new Message( yield $this->protocolWriter->write(
new AdvancedJsonRpc\Request($id, $method, (object)$params) new Message(
) new AdvancedJsonRpc\Request($id, $method, (object) $params)
); )
);
$promise->onResolve( $deferred = new Deferred();
/**
* @return Promise
*/
function () use ($id) {
$deferred = new \Amp\Deferred();
$listener = $listener =
/**
* @param callable $listener
* @return void
*/
function (Message $msg) use ($id, $deferred, &$listener) {
error_log('request handler');
/** /**
* @param callable $listener * @psalm-suppress UndefinedPropertyFetch
* @return void * @psalm-suppress MixedArgument
*/ */
function (Message $msg) use ($id, $deferred, &$listener) { if ($msg->body
/** && AdvancedJsonRpc\Response::isResponse($msg->body)
* @psalm-suppress UndefinedPropertyFetch && $msg->body->id === $id
* @psalm-suppress MixedArgument ) {
*/ // Received a response
if ($msg->body $this->protocolReader->removeListener('message', $listener);
&& AdvancedJsonRpc\Response::isResponse($msg->body) if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
&& $msg->body->id === $id $deferred->resolve($msg->body->result);
) { } else {
// Received a response $deferred->fail($msg->body->error);
$this->protocolReader->removeListener('message', $listener);
if (AdvancedJsonRpc\SuccessResponse::isSuccessResponse($msg->body)) {
$deferred->resolve($msg->body->result);
} else {
$deferred->fail($msg->body->error);
}
} }
}; }
$this->protocolReader->on('message', $listener); };
return $deferred->promise(); $this->protocolReader->on('message', $listener);
} return $deferred->promise();
); });
return $promise;
} }
/** /**

View File

@ -26,9 +26,10 @@ use Psalm\Internal\LanguageServer\Server\TextDocument;
use LanguageServerProtocol\{Range, Position, Diagnostic, DiagnosticSeverity}; use LanguageServerProtocol\{Range, Position, Diagnostic, DiagnosticSeverity};
use AdvancedJsonRpc; use AdvancedJsonRpc;
use Amp\Promise; use Amp\Promise;
use function Amp\coroutine;
use Throwable; use Throwable;
use Webmozart\PathUtil\Path; use Webmozart\PathUtil\Path;
use function Amp\call;
use function Amp\asyncCoroutine;
/** /**
* @internal * @internal
@ -100,70 +101,60 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
$this->protocolReader->on( $this->protocolReader->on(
'message', 'message',
/** @return void */ /** @return void */
function (Message $msg) { asyncCoroutine(function (Message $msg) {
\Amp\call( if (!$msg->body) {
/** @return \Generator<int, Promise, mixed, void> */ return;
function () use ($msg) { }
if (!$msg->body) {
return;
}
// Ignore responses, this is the handler for requests and notifications // Ignore responses, this is the handler for requests and notifications
if (AdvancedJsonRpc\Response::isResponse($msg->body)) { if (AdvancedJsonRpc\Response::isResponse($msg->body)) {
return; return;
} }
$result = null; $result = null;
$error = null; $error = null;
try { try {
// Invoke the method handler to get a result // Invoke the method handler to get a result
/** /**
* @var Promise * @var Promise
* @psalm-suppress UndefinedClass * @psalm-suppress UndefinedClass
*/ */
$dispatched = $this->dispatch($msg->body); $dispatched = $this->dispatch($msg->body);
$result = yield $dispatched; $result = yield $dispatched;
} catch (AdvancedJsonRpc\Error $e) { } catch (AdvancedJsonRpc\Error $e) {
// If a ResponseError is thrown, send it back in the Response // If a ResponseError is thrown, send it back in the Response
$error = $e; $error = $e;
} catch (Throwable $e) { } catch (Throwable $e) {
// If an unexpected error occurred, send back an INTERNAL_ERROR error response // If an unexpected error occurred, send back an INTERNAL_ERROR error response
$error = new AdvancedJsonRpc\Error( $error = new AdvancedJsonRpc\Error(
(string)$e, (string) $e,
AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR, AdvancedJsonRpc\ErrorCode::INTERNAL_ERROR,
null, null,
$e $e
); );
} }
// Only send a Response for a Request // Only send a Response for a Request
// Notifications do not send Responses // Notifications do not send Responses
/** /**
* @psalm-suppress UndefinedPropertyFetch * @psalm-suppress UndefinedPropertyFetch
* @psalm-suppress MixedArgument * @psalm-suppress MixedArgument
*/ */
if (AdvancedJsonRpc\Request::isRequest($msg->body)) { if (AdvancedJsonRpc\Request::isRequest($msg->body)) {
if ($error !== null) { if ($error !== null) {
$responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error); $responseBody = new AdvancedJsonRpc\ErrorResponse($msg->body->id, $error);
} else { } else {
$responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result); $responseBody = new AdvancedJsonRpc\SuccessResponse($msg->body->id, $result);
}
$this->protocolWriter->write(new Message($responseBody));
}
} }
); yield $this->protocolWriter->write(new Message($responseBody));
} }
})
); );
$this->protocolReader->on( $this->protocolReader->on(
'readMessageGroup', 'readMessageGroup',
/** @return void */ /** @return void */
function () { function () {
\Amp\call( $this->doAnalysis();
/** @return null */
function () {
$this->doAnalysis();
}
);
} }
); );
@ -186,7 +177,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
string $rootPath = null, string $rootPath = null,
int $processId = null int $processId = null
): Promise { ): Promise {
return \Amp\call( return call(
/** @return \Generator<int, true, mixed, InitializeResult> */ /** @return \Generator<int, true, mixed, InitializeResult> */
function () use ($capabilities, $rootPath, $processId) { function () use ($capabilities, $rootPath, $processId) {
// Eventually, this might block on something. Leave it as a generator. // Eventually, this might block on something. Leave it as a generator.
@ -442,22 +433,4 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher
} }
return $filepath; return $filepath;
} }
/**
* Throws an exception on the next tick.
* Useful for letting a promise crash the process on rejection.
*
* @param Throwable $err
* @return void
* @psalm-suppress PossiblyUnusedMethod
*/
public static function crash(Throwable $err)
{
\Amp\Loop::defer(
/** @return void */
function () use ($err) {
throw $err;
}
);
}
} }

View File

@ -4,9 +4,9 @@ declare(strict_types = 1);
namespace Psalm\Internal\LanguageServer; namespace Psalm\Internal\LanguageServer;
use AdvancedJsonRpc\Message as MessageBody; use AdvancedJsonRpc\Message as MessageBody;
use Amp\ByteStream\ResourceInputStream;
use Exception; use Exception;
use Psalm\Internal\LanguageServer\Message; use function Amp\asyncCall;
use Amp\Loop;
/** /**
* Source: https://github.com/felixfbecker/php-language-server/tree/master/src/ProtocolStreamReader.php * Source: https://github.com/felixfbecker/php-language-server/tree/master/src/ProtocolStreamReader.php
@ -18,11 +18,6 @@ class ProtocolStreamReader implements ProtocolReader
const PARSE_HEADERS = 1; const PARSE_HEADERS = 1;
const PARSE_BODY = 2; const PARSE_BODY = 2;
/** @var resource */
private $input;
/** @var string */
private $read_watcher;
/** /**
* This is checked by ProtocolStreamReader so that it will stop reading from streams in the forked process. * This is checked by ProtocolStreamReader so that it will stop reading from streams in the forked process.
* There could be buffered bytes in stdin/over TCP, those would be processed by TCP if it were not for this check. * There could be buffered bytes in stdin/over TCP, those would be processed by TCP if it were not for this check.
@ -45,49 +40,36 @@ class ProtocolStreamReader implements ProtocolReader
*/ */
public function __construct($input) public function __construct($input)
{ {
$this->input = $input; $input = new ResourceInputStream($input);
asyncCall(function () use ($input): \Generator {
$read_watcher = Loop::onReadable( while (($chunk = yield $input->read()) !== null) {
$this->input, /** @var string $chunk */
/** @return void */ $this->readMessages($chunk);
function () {
if (feof($this->input)) {
// If stream_select reported a status change for this stream,
// but the stream is EOF, it means it was closed.
$this->emitClose();
return;
}
if (!$this->is_accepting_new_requests) {
// If we fork, don't read any bytes in the input buffer from the worker process.
$this->emitClose();
return;
}
$emitted_messages = $this->readMessages();
if ($emitted_messages > 0) {
$this->emit('readMessageGroup');
}
} }
);
$this->read_watcher = $read_watcher; $this->emitClose();
});
$this->on( $this->on(
'close', 'close',
/** @return void */ /** @return void */
function () { static function () use ($input) {
Loop::cancel($this->read_watcher); $input->close();
} }
); );
} }
/** /**
* @param string $buffer
*
* @return int * @return int
*/ */
private function readMessages() : int private function readMessages(string $buffer) : int
{ {
$emitted_messages = 0; $emitted_messages = 0;
while (($c = fgetc($this->input)) !== false && $c !== '') { $i = 0;
$this->buffer .= $c; while (($buffer[$i] ?? '') !== '') {
$this->buffer .= $buffer[$i++];
switch ($this->parsing_mode) { switch ($this->parsing_mode) {
case self::PARSE_HEADERS: case self::PARSE_HEADERS:
if ($this->buffer === "\r\n") { if ($this->buffer === "\r\n") {

View File

@ -3,13 +3,8 @@ declare(strict_types = 1);
namespace Psalm\Internal\LanguageServer; namespace Psalm\Internal\LanguageServer;
use Psalm\Internal\LanguageServer\Message; use Amp\ByteStream\ResourceOutputStream;
use Amp\{ use Amp\Promise;
Deferred,
Loop,
Promise
};
use RuntimeException;
/** /**
* @internal * @internal
@ -17,26 +12,16 @@ use RuntimeException;
class ProtocolStreamWriter implements ProtocolWriter class ProtocolStreamWriter implements ProtocolWriter
{ {
/** /**
* @var resource $output * @var \Amp\ByteStream\ResourceOutputStream
*/ */
private $output; private $output;
/**
* @var ?string
*/
private $output_watcher;
/**
* @var array<int, array{message: string, deferred: Deferred}> $messages
*/
private $messages = [];
/** /**
* @param resource $output * @param resource $output
*/ */
public function __construct($output) public function __construct($output)
{ {
$this->output = $output; $this->output = new ResourceOutputStream($output);
} }
/** /**
@ -44,58 +29,6 @@ class ProtocolStreamWriter implements ProtocolWriter
*/ */
public function write(Message $msg): Promise public function write(Message $msg): Promise
{ {
// if the message queue is currently empty, register a write handler. return $this->output->write((string)$msg);
if (empty($this->messages)) {
$this->output_watcher = Loop::onWritable(
$this->output,
/** @return void */
function () {
$this->flush();
}
);
}
$deferred = new \Amp\Deferred();
$this->messages[] = [
'message' => (string)$msg,
'deferred' => $deferred
];
return $deferred->promise();
}
/**
* Writes pending messages to the output stream.
*
* @return void
*/
private function flush()
{
$keepWriting = true;
while ($keepWriting) {
$message = $this->messages[0]['message'];
$deferred = $this->messages[0]['deferred'];
$bytesWritten = @fwrite($this->output, $message);
if ($bytesWritten > 0) {
$message = substr($message, $bytesWritten);
}
// Determine if this message was completely sent
if (strlen($message) === 0) {
array_shift($this->messages);
// This was the last message in the queue, remove the write handler.
if (count($this->messages) === 0 && $this->output_watcher) {
Loop::cancel($this->output_watcher);
$keepWriting = false;
}
$deferred->resolve();
} else {
$this->messages[0]['message'] = $message;
$keepWriting = false;
}
}
} }
} }

View File

@ -42,7 +42,7 @@ use Psalm\Internal\LanguageServer\Index\ReadableIndex;
use Psalm\Internal\Analyzer\FileAnalyzer; use Psalm\Internal\Analyzer\FileAnalyzer;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer; use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Amp\Promise; use Amp\Promise;
use function Amp\coroutine; use Amp\Success;
use function Psalm\Internal\LanguageServer\{waitForEvent, isVendored}; use function Psalm\Internal\LanguageServer\{waitForEvent, isVendored};
/** /**
@ -174,45 +174,36 @@ class TextDocument
*/ */
public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise public function definition(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return \Amp\call( $file_path = LanguageServer::uriToPath($textDocument->uri);
/**
* @return \Generator<int, true, mixed, Hover|Location>
*/
function () use ($textDocument, $position) {
if (false) {
yield true;
}
$file_path = LanguageServer::uriToPath($textDocument->uri); try {
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position);
} catch (\Psalm\Exception\UnanalyzedFileException $e) {
$this->codebase->file_provider->openFile($file_path);
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
return new Success(new Hover([]));
}
try { if ($reference_location === null) {
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); return new Success(new Hover([]));
} catch (\Psalm\Exception\UnanalyzedFileException $e) { }
$this->codebase->file_provider->openFile($file_path);
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
return new Hover([]);
}
if ($reference_location === null) { list($reference) = $reference_location;
return new Hover([]);
}
list($reference) = $reference_location; $code_location = $this->codebase->getSymbolLocation($file_path, $reference);
$code_location = $this->codebase->getSymbolLocation($file_path, $reference); if (!$code_location) {
return new Success(new Hover([]));
}
if (!$code_location) { return new Success(
return new Hover([]); new Location(
} LanguageServer::pathToUri($code_location->file_path),
new Range(
return new Location( new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1),
LanguageServer::pathToUri($code_location->file_path), new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1)
new Range( )
new Position($code_location->getLineNumber() - 1, $code_location->getColumn() - 1), )
new Position($code_location->getEndLineNumber() - 1, $code_location->getEndColumn() - 1)
)
);
}
); );
} }
@ -226,40 +217,29 @@ class TextDocument
*/ */
public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise public function hover(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return \Amp\call( $file_path = LanguageServer::uriToPath($textDocument->uri);
/**
* @return \Generator<int, true, mixed, Hover>
*/
function () use ($textDocument, $position) {
if (false) {
yield true;
}
$file_path = LanguageServer::uriToPath($textDocument->uri); try {
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position);
} catch (\Psalm\Exception\UnanalyzedFileException $e) {
$this->codebase->file_provider->openFile($file_path);
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
return new Success(new Hover([]));
}
try { if ($reference_location === null) {
$reference_location = $this->codebase->getReferenceAtPosition($file_path, $position); return new Success(new Hover([]));
} catch (\Psalm\Exception\UnanalyzedFileException $e) { }
$this->codebase->file_provider->openFile($file_path);
$this->server->queueFileAnalysis($file_path, $textDocument->uri);
return new Hover([]);
}
if ($reference_location === null) { list($reference, $range) = $reference_location;
return new Hover([]);
}
list($reference, $range) = $reference_location; $contents = [];
$contents[] = new MarkedString(
$contents = []; 'php',
$contents[] = new MarkedString( $this->codebase->getSymbolInformation($file_path, $reference)
'php',
$this->codebase->getSymbolInformation($file_path, $reference)
);
return new Hover($contents, $range);
}
); );
return new Success(new Hover($contents, $range));
} }
/** /**
@ -278,92 +258,81 @@ class TextDocument
*/ */
public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise public function completion(TextDocumentIdentifier $textDocument, Position $position): Promise
{ {
return \Amp\call( $file_path = LanguageServer::uriToPath($textDocument->uri);
/**
* @return \Generator<int, true, mixed, array<empty, empty>|CompletionList> $completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position);
*/
function () use ($textDocument, $position) { if (!$completion_data) {
if (false) { error_log('completion not found at ' . $position->line . ':' . $position->character);
yield true; return new Success([]);
}
list($recent_type, $gap) = $completion_data;
error_log('gap: "' . $gap . '" and type: "' . $recent_type . '"');
$completion_items = [];
if ($gap === '->' || $gap === '::') {
$instance_completion_items = [];
$static_completion_items = [];
try {
$class_storage = $this->codebase->classlike_storage_provider->get($recent_type);
foreach ($class_storage->appearing_method_ids as $declaring_method_id) {
$method_storage = $this->codebase->methods->getStorage($declaring_method_id);
$instance_completion_items[] = new CompletionItem(
(string)$method_storage,
CompletionItemKind::METHOD,
null,
null,
null,
null,
$method_storage->cased_name . '()'
);
} }
$file_path = LanguageServer::uriToPath($textDocument->uri); foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
$property_storage = $this->codebase->properties->getStorage(
$declaring_class . '::$' . $property_name
);
$completion_data = $this->codebase->getCompletionDataAtPosition($file_path, $position); $instance_completion_items[] = new CompletionItem(
$property_storage->getInfo() . ' $' . $property_name,
if (!$completion_data) { CompletionItemKind::PROPERTY,
error_log('completion not found at ' . $position->line . ':' . $position->character); null,
return []; null,
null,
null,
($gap === '::' ? '$' : '') . $property_name
);
} }
list($recent_type, $gap) = $completion_data; foreach ($class_storage->class_constant_locations as $const_name => $_) {
$static_completion_items[] = new CompletionItem(
error_log('gap: "' . $gap . '" and type: "' . $recent_type . '"'); 'const ' . $const_name,
CompletionItemKind::VARIABLE,
$completion_items = []; null,
null,
if ($gap === '->' || $gap === '::') { null,
$instance_completion_items = []; null,
$static_completion_items = []; $const_name
);
try {
$class_storage = $this->codebase->classlike_storage_provider->get($recent_type);
foreach ($class_storage->appearing_method_ids as $declaring_method_id) {
$method_storage = $this->codebase->methods->getStorage($declaring_method_id);
$instance_completion_items[] = new CompletionItem(
(string)$method_storage,
CompletionItemKind::METHOD,
null,
null,
null,
null,
$method_storage->cased_name . '()'
);
}
foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) {
$property_storage = $this->codebase->properties->getStorage(
$declaring_class . '::$' . $property_name
);
$instance_completion_items[] = new CompletionItem(
$property_storage->getInfo() . ' $' . $property_name,
CompletionItemKind::PROPERTY,
null,
null,
null,
null,
($gap === '::' ? '$' : '') . $property_name
);
}
foreach ($class_storage->class_constant_locations as $const_name => $_) {
$static_completion_items[] = new CompletionItem(
'const ' . $const_name,
CompletionItemKind::VARIABLE,
null,
null,
null,
null,
$const_name
);
}
} catch (\Exception $e) {
error_log($e->getMessage());
return [];
}
$completion_items = $gap === '->'
? $instance_completion_items
: array_merge($instance_completion_items, $static_completion_items);
error_log('Found ' . count($completion_items) . ' items');
} }
} catch (\Exception $e) {
return new CompletionList($completion_items, false); error_log($e->getMessage());
return new Success([]);
} }
);
$completion_items = $gap === '->'
? $instance_completion_items
: array_merge($instance_completion_items, $static_completion_items);
error_log('Found ' . count($completion_items) . ' items');
}
return new Success(new CompletionList($completion_items, false));
} }
} }

View File

@ -0,0 +1,41 @@
<?php
namespace Amp;
/**
* @template TReturn
* @param callable():\Generator<mixed, mixed, mixed, TReturn> $gen
* @return Promise<TReturn>
*/
function coroutine(callable $gen) : Promise {}
/**
* @template TReturn
* @param callable():(\Generator<mixed, mixed, mixed, TReturn>|null) $gen
* @return Promise<TReturn>
*/
function call(callable $gen) : Promise {}
/**
* @template TReturn
*/
interface Promise {
/**
* @param callable(\Throwable|null $exception, TReturn|null $result):void
* @return void
*/
function onResolve(callable $onResolved);
}
/**
* @template TReturn
*
* @template-implements Promise<TReturn>
*/
class Success implements Promise {
/**
* @param callable(\Throwable|null $exception, TReturn|null $result):void
* @return void
*/
function onResolve(callable $onResolved) {}
}

View File

@ -1,27 +0,0 @@
<?php
namespace Amp;
/**
* @template TReturn
* @param callable():\Generator<mixed, mixed, mixed, TReturn> $gen
* @return Promise<TReturn>
*/
function coroutine(callable $gen) : Promise {}
/**
* @template TReturn
* @param callable():(\Generator<mixed, mixed, mixed, TReturn>|null) $gen
* @return Promise<TReturn>
*/
function call(callable $gen) : Promise {}
/**
* @template TReturn
*/
class Promise {
/**
* @return TReturn
*/
function wait() {}
}