From e1947faa1abae80fd47ea7a185fd8422ebde8fb4 Mon Sep 17 00:00:00 2001 From: chriseth Date: Thu, 16 Dec 2021 13:48:23 +0100 Subject: [PATCH] LSP for solcjs --- libsolc/CMakeLists.txt | 3 ++ libsolc/libsolc.cpp | 57 ++++++++++++++++++++++++++ libsolc/libsolc.h | 34 +++++++++++++++ libsolidity/lsp/LanguageServer.cpp | 65 +++++++++++++++-------------- libsolidity/lsp/LanguageServer.h | 11 +++++ libsolidity/lsp/Transport.cpp | 66 ++++++++++++++++++++++++++++++ libsolidity/lsp/Transport.h | 39 ++++++++++++++++++ 7 files changed, 245 insertions(+), 30 deletions(-) diff --git a/libsolc/CMakeLists.txt b/libsolc/CMakeLists.txt index fce54dd24..10246ec21 100644 --- a/libsolc/CMakeLists.txt +++ b/libsolc/CMakeLists.txt @@ -7,6 +7,9 @@ if (EMSCRIPTEN) solidity_alloc solidity_free solidity_reset + solidity_lsp_start + solidity_lsp_send + solidity_lsp_send_receive ) # Specify which functions to export in soljson.js. # Note that additional Emscripten-generated methods needed by solc-js are diff --git a/libsolc/libsolc.cpp b/libsolc/libsolc.cpp index 48cf7e0f8..95468eb79 100644 --- a/libsolc/libsolc.cpp +++ b/libsolc/libsolc.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -112,6 +114,9 @@ string compile(string _input, CStyleReadFileCallback _readCallback, void* _readC return compiler.compile(move(_input)); } +unique_ptr languageServerTransport; +unique_ptr languageServer; + } extern "C" @@ -132,6 +137,56 @@ extern char* solidity_compile(char const* _input, CStyleReadFileCallback _readCa return solidityAllocations.emplace_back(compile(_input, _readCallback, _readContext)).data(); } +extern int solidity_lsp_start(CStyleReadFileCallback /*TODO(pr) _readCallback*/, void* /*_readContext*/) noexcept +{ + if (languageServer || languageServerTransport) + return -1; + + languageServerTransport = make_unique(); + languageServer = make_unique(*languageServerTransport); + return 0; +} + +extern int solidity_lsp_send(char const* _input) noexcept +{ + if (!languageServer) + return -1; + + if (languageServer->isRunning()) + return -2; + + solAssert(languageServerTransport, ""); + + std::string errors{}; + Json::Value jsonMessage{}; + if (!jsonParseStrict(_input, jsonMessage, &errors)) + return -3; + + languageServerTransport->appendInput(jsonMessage); + languageServer->runIteration(); + return 0; +} + +extern char const* solidity_try_receive() noexcept +{ + if (!languageServerTransport) + return ""; + + optional messageJson = languageServerTransport->popOutput(); + if (!messageJson) + return ""; + + return solidityAllocations.emplace_back(jsonPrettyPrint(messageJson.value())).data(); +} + +extern char const* solidity_lsp_send_receive(char const* _input) noexcept +{ + if (solidity_lsp_send(_input) < 0) + return ""; + + return solidity_try_receive(); +} + extern char* solidity_alloc(size_t _size) noexcept { try @@ -156,5 +211,7 @@ extern void solidity_reset() noexcept // can be freed here. yul::YulStringRepository::reset(); solidityAllocations.clear(); + languageServer.reset(); + languageServerTransport.reset(); } } diff --git a/libsolc/libsolc.h b/libsolc/libsolc.h index c5b3b3db7..691157d88 100644 --- a/libsolc/libsolc.h +++ b/libsolc/libsolc.h @@ -90,6 +90,40 @@ void solidity_free(char* _data) SOLC_NOEXCEPT; /// @returns A pointer to the result. The pointer returned must be freed by the caller using solidity_free() or solidity_reset(). char* solidity_compile(char const* _input, CStyleReadFileCallback _readCallback, void* _readContext) SOLC_NOEXCEPT; +/// @TODO the biggest problem here is that the callback has to be synchronous. +/// With "compile" we would just record requested files and trigger a recompile later, +/// so we might need a way to trigger a recompile. + +/// Switch into LSP mode. Can be undone using solidity_reset(). +/// +/// @returns 0 on success and -1 on failure. A failure can only happen due to mis-use of the API, +/// such as, the LSP mode has been already initiated. +int solidity_lsp_start(CStyleReadFileCallback _readCallback, void* _readContext) SOLC_NOEXCEPT; + +/// Sends a single JSON-RPC message to the LSP server. +/// This message must not include any HTTP headers but only hold the payload. +/// +/// @retval 0 Success. +/// @retval -1 Server not initialized. +/// @retval -2 Server not running (e.g. termination initiated). +/// @retval -3 Could not parse JSON RPC message. +int solidity_lsp_send(char const* _input) SOLC_NOEXCEPT; + +/// Tries to pop a pending message from the LSP server. +/// @returns either an empty string if not possible +/// (e.g. no message available or server shut down) or a stringified JSON response message. +char const* solidity_try_receive() SOLC_NOEXCEPT; + +/// Send one or more JSON-RPC messages to the LSP (including the HTTP headers), +/// expecting a response. +/// If the input is empty, just checks for a pending response. +/// @returns JSON-RPC message (inculding HTTP headers), can be empty (or nullptr). +/// If the result is not null, it has to be freed by the caller using solidity_free. +/// +/// This can cause the callback provided in solidity_lsp to be invoked. +/// Should only be called after having called solidity_lsp. +char const *solidity_lsp_send_receive(char const* _input) SOLC_NOEXCEPT; + /// Frees up any allocated memory. /// /// NOTE: the pointer returned by solidity_compile as well as any other pointer retrieved via solidity_alloc() diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index 671476ab0..cfae73bd2 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -68,6 +68,7 @@ int toDiagnosticSeverity(Error::Type _errorType) } +// TODO provide constructor with custom read callback LanguageServer::LanguageServer(Transport& _transport): m_client{_transport}, m_handlers{ @@ -180,37 +181,9 @@ void LanguageServer::compileAndUpdateDiagnostics() bool LanguageServer::run() { - while (m_state != State::ExitRequested && m_state != State::ExitWithoutShutdown && !m_client.closed()) - { - MessageID id; - try - { - optional const jsonMessage = m_client.receive(); - if (!jsonMessage) - continue; + while (isRunning()) + runIteration(); - if ((*jsonMessage)["method"].isString()) - { - string const methodName = (*jsonMessage)["method"].asString(); - id = (*jsonMessage)["id"]; - - if (auto handler = util::valueOrDefault(m_handlers, methodName)) - handler(id, (*jsonMessage)["params"]); - else - m_client.error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName); - } - else - m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string."); - } - catch (RequestError const& error) - { - m_client.error(id, error.code(), error.comment() ? *error.comment() : ""s); - } - catch (...) - { - m_client.error(id, ErrorCode::InternalError, "Unhandled exception: "s + boost::current_exception_diagnostic_information()); - } - } return m_state == State::ExitRequested; } @@ -223,6 +196,38 @@ void LanguageServer::requireServerInitialized() ); } +bool LanguageServer::runIteration() +{ + MessageID id; + try + { + optional const jsonMessage = m_client.receive(); + if (!jsonMessage) + return true; + + if ((*jsonMessage)["method"].isString()) + { + string const methodName = (*jsonMessage)["method"].asString(); + id = (*jsonMessage)["id"]; + + if (auto handler = util::valueOrDefault(m_handlers, methodName)) + handler(id, (*jsonMessage)["params"]); + else + m_client.error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName); + } + } + catch (RequestError const& error) + { + m_client.error(id, error.code(), error.comment() ? *error.comment() : ""s); + } + catch (...) + { + m_client.error(id, ErrorCode::InternalError, "Unhandled exception: "s + boost::current_exception_diagnostic_information()); + } + + return isRunning(); +} + void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) { lspAssert( diff --git a/libsolidity/lsp/LanguageServer.h b/libsolidity/lsp/LanguageServer.h index d2ad09367..641be796c 100644 --- a/libsolidity/lsp/LanguageServer.h +++ b/libsolidity/lsp/LanguageServer.h @@ -62,6 +62,17 @@ public: frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos); langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_compilerStack; } + /// Run a single iteration of processing inputs and generating outputs. + /// To be used when we are not in control of the event loop. + /// @returns false if the process is supposed to terminate. + bool runIteration(); + + /// @returns true if the server has not terminated yet, false otherwise. + bool isRunning() const noexcept + { + return m_state != State::ExitRequested && m_state != State::ExitWithoutShutdown && !m_client.closed(); + } + private: /// Checks if the server is initialized (to be used by messages that need it to be initialized). /// Reports an error and returns false if not. diff --git a/libsolidity/lsp/Transport.cpp b/libsolidity/lsp/Transport.cpp index fcd3c8249..5ef978668 100644 --- a/libsolidity/lsp/Transport.cpp +++ b/libsolidity/lsp/Transport.cpp @@ -30,6 +30,72 @@ using namespace std; using namespace solidity::lsp; +namespace +{ +template +optional popFromFront(std::list& _queue) +{ + if (_queue.empty()) + return nullopt; + Json::Value message = _queue.front(); + _queue.pop_front(); + return message; +} +} + +bool MockTransport::closed() const noexcept +{ + return m_closed; +} + +void MockTransport::appendInput(Json::Value _message) +{ + solAssert(!m_closed, ""); + m_input.emplace_back(move(_message)); +} + +optional MockTransport::receive() +{ + return popFromFront(m_input); +} + +optional MockTransport::popOutput() +{ + return popFromFront(m_output); +} + +void MockTransport::notify(string _method, Json::Value _message) +{ + Json::Value json; + json["method"] = move(_method); + json["params"] = move(_message); + send(move(json)); +} + +void MockTransport::reply(MessageID _id, Json::Value _message) +{ + Json::Value json; + json["result"] = move(_message); + send(move(json), _id); +} + +void MockTransport::error(MessageID _id, ErrorCode _code, string _message) +{ + Json::Value json; + json["error"]["code"] = static_cast(_code); + json["error"]["message"] = move(_message); + send(move(json), _id); +} + +void MockTransport::send(Json::Value _json, MessageID _id) +{ + _json["jsonrpc"] = "2.0"; + if (_id != Json::nullValue) + _json["id"] = _id; + + m_output.push_back(_json); +} + IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out): m_input{_in}, m_output{_out} diff --git a/libsolidity/lsp/Transport.h b/libsolidity/lsp/Transport.h index 82fe43909..6642549e1 100644 --- a/libsolidity/lsp/Transport.h +++ b/libsolidity/lsp/Transport.h @@ -17,12 +17,14 @@ // SPDX-License-Identifier: GPL-3.0 #pragma once +#include #include #include #include #include +#include #include #include #include @@ -91,6 +93,26 @@ public: virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0; }; +class MockTransport: public Transport +{ +public: + void close() { m_closed = true; } + bool closed() const noexcept override; + std::optional receive() override; + void notify(std::string _method, Json::Value _params) override; + void reply(MessageID _id, Json::Value _result) override; + void error(MessageID _id, ErrorCode _code, std::string _message) override; + + void send(Json::Value _message, MessageID _id = Json::nullValue); + std::optional popOutput(); + void appendInput(Json::Value _message); + +private: + bool m_closed = false; + std::list m_input {}; + std::list m_output {}; +}; + /** * LSP Transport using JSON-RPC over iostreams. */ @@ -126,4 +148,21 @@ private: std::ostream& m_output; }; +/** + * LSP Transport using pure string buffers. + * Used by solcjs. + */ +class BufferedTransport: public IOStreamTransport +{ +public: + BufferedTransport(): IOStreamTransport(m_input, m_output) {} + + void appendInput(char const* _input) { m_input.write(_input, static_cast(strlen(_input))); } + std::string popOutput() { return util::readUntilEnd(m_output); } + +private: + std::stringstream m_input; + std::stringstream m_output; +}; + }