From 5432c5cdb0c95fed390bb08d6b4e35665251a7ad Mon Sep 17 00:00:00 2001 From: Christian Parpart Date: Wed, 12 Oct 2022 16:57:11 +0200 Subject: [PATCH] Language Server: Constrain server feature set based on advertised client capabilities. --- Changelog.md | 1 + libsolidity/lsp/GotoDefinition.cpp | 18 ++++++ libsolidity/lsp/GotoDefinition.h | 3 + libsolidity/lsp/HandlerBase.cpp | 15 +++++ libsolidity/lsp/HandlerBase.h | 20 +++++-- libsolidity/lsp/LanguageServer.cpp | 96 ++++++++++++++++++++++-------- libsolidity/lsp/LanguageServer.h | 20 +++++-- libsolidity/lsp/RenameSymbol.cpp | 14 +++++ libsolidity/lsp/RenameSymbol.h | 3 + test/lsp.py | 8 ++- 10 files changed, 161 insertions(+), 37 deletions(-) diff --git a/Changelog.md b/Changelog.md index 98b36aab8..670d1ceb0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -7,6 +7,7 @@ Compiler Features: * Commandline Interface: Add `--no-cbor-metadata` that skips CBOR metadata from getting appended at the end of the bytecode. * Standard JSON: Add a boolean field `settings.metadata.appendCBOR` that skips CBOR metadata from getting appended at the end of the bytecode. * Yul Optimizer: Allow replacing the previously hard-coded cleanup sequence by specifying custom steps after a colon delimiter (``:``) in the sequence string. +* Language Server: Constrain server feature set based on advertised client capabilities. Bugfixes: diff --git a/libsolidity/lsp/GotoDefinition.cpp b/libsolidity/lsp/GotoDefinition.cpp index 532792c17..01fbd146b 100644 --- a/libsolidity/lsp/GotoDefinition.cpp +++ b/libsolidity/lsp/GotoDefinition.cpp @@ -16,6 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 #include +#include #include // for RequestError #include #include @@ -32,6 +33,23 @@ using namespace solidity::langutil; using namespace solidity::lsp; using namespace std; +Json::Value GotoDefinition::onReportCapabilities(Json::Value const& _clientCapabilities) +{ + Json::Value replyCapabilities = Json::objectValue; + if (_clientCapabilities["textDocument"]["definition"]) + { + replyCapabilities["definitionProvider"] = true; + m_server.registerHandler("textDocument/definition", *this); + } + + if (_clientCapabilities["textDocument"]["implementation"]) + { + replyCapabilities["implementationProvider"] = true; + m_server.registerHandler("textDocument/implementation", *this); + } + return replyCapabilities; +} + void GotoDefinition::operator()(MessageID _id, Json::Value const& _args) { auto const [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args); diff --git a/libsolidity/lsp/GotoDefinition.h b/libsolidity/lsp/GotoDefinition.h index 453da3f15..2a1b04f27 100644 --- a/libsolidity/lsp/GotoDefinition.h +++ b/libsolidity/lsp/GotoDefinition.h @@ -16,6 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 #include +#include namespace solidity::lsp { @@ -25,6 +26,8 @@ class GotoDefinition: public HandlerBase public: explicit GotoDefinition(LanguageServer& _server): HandlerBase(_server) {} + Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override; + void operator()(MessageID, Json::Value const&); }; diff --git a/libsolidity/lsp/HandlerBase.cpp b/libsolidity/lsp/HandlerBase.cpp index f40f189ab..8f408e6de 100644 --- a/libsolidity/lsp/HandlerBase.cpp +++ b/libsolidity/lsp/HandlerBase.cpp @@ -31,6 +31,21 @@ using namespace solidity::lsp; using namespace solidity::util; using namespace std; +CharStreamProvider const& HandlerBase::charStreamProvider() const noexcept +{ + return m_server.compilerStack(); +} + +FileRepository& HandlerBase::fileRepository() const noexcept +{ + return m_server.fileRepository(); +} + +Transport& HandlerBase::client() const noexcept +{ + return m_server.client(); +} + Json::Value HandlerBase::toRange(SourceLocation const& _location) const { if (!_location.hasText()) diff --git a/libsolidity/lsp/HandlerBase.h b/libsolidity/lsp/HandlerBase.h index e2ccb88d8..f88aea7a5 100644 --- a/libsolidity/lsp/HandlerBase.h +++ b/libsolidity/lsp/HandlerBase.h @@ -18,17 +18,19 @@ #pragma once #include -#include #include #include +#include + #include namespace solidity::lsp { class Transport; +class LanguageServer; /** * Helper base class for implementing handlers. @@ -37,6 +39,16 @@ class HandlerBase { public: explicit HandlerBase(LanguageServer& _server): m_server{_server} {} + virtual ~HandlerBase() = default; + + /// Callback to be invoked on every custom handler that can be used to decide whether to enable + /// or disable this feature on the server side. + /// + /// @param _clientCapabilities JSON node to the root of the client advertised capabilities to be used + /// to decide if the implemented feature should be enabled or not. + /// @returns JSON object, with mapping to the capabilities to reply back with. + /// Use this to write the capabilities-reply with respect the implementation. + virtual Json::Value onReportCapabilities(Json::Value const& /*_clientCapabilities*/) { return Json::objectValue; } Json::Value toRange(langutil::SourceLocation const& _location) const; Json::Value toJson(langutil::SourceLocation const& _location) const; @@ -45,9 +57,9 @@ public: /// from the JSON-RPC parameters. std::pair extractSourceUnitNameAndLineColumn(Json::Value const& _params) const; - langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.compilerStack(); } - FileRepository& fileRepository() const noexcept { return m_server.fileRepository(); } - Transport& client() const noexcept { return m_server.client(); } + langutil::CharStreamProvider const& charStreamProvider() const noexcept; + FileRepository& fileRepository() const noexcept; + Transport& client() const noexcept; protected: LanguageServer& m_server; diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index f868a022a..35fdc9457 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -128,11 +128,57 @@ Json::Value semanticTokensLegend() return legend; } +class SemanticTokensHandler: public HandlerBase +{ +public: + using HandlerBase::HandlerBase; + Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override; + void operator()(MessageID _id, Json::Value const& _args); +}; + +Json::Value SemanticTokensHandler::onReportCapabilities(Json::Value const& _clientCapabilities) +{ + Json::Value replyCapabilities = Json::objectValue; + + if (_clientCapabilities["textDocument"]["semanticTokens"]) + { + replyCapabilities["semanticTokensProvider"]["legend"] = semanticTokensLegend(); + replyCapabilities["semanticTokensProvider"]["range"] = false; + replyCapabilities["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true + + m_server.registerHandler("textDocument/semanticTokens/full", *this); + } + + return replyCapabilities; +} + +void SemanticTokensHandler::operator()(MessageID _id, Json::Value const& _args) +{ + auto const uri = _args["textDocument"]["uri"]; + + m_server.compile(); + + auto const sourceName = fileRepository().uriToSourceUnitName(uri.as()); + SourceUnit const& ast = m_server.compilerStack().ast(sourceName); + charStreamProvider().charStream(sourceName); + + Json::Value data = SemanticTokensBuilder().build(ast, charStreamProvider().charStream(sourceName)); + + Json::Value reply = Json::objectValue; + reply["data"] = data; + + client().reply(_id, std::move(reply)); +} + } LanguageServer::LanguageServer(Transport& _transport): - m_client{_transport}, - m_handlers{ + m_onDemandHandlers{ + make_unique(*this), + make_unique(*this), + make_unique(*this), + }, + m_coreHandlers{ {"$/cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, {"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, {"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }}, @@ -146,14 +192,20 @@ LanguageServer::LanguageServer(Transport& _transport): {"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)}, {"textDocument/rename", RenameSymbol(*this) }, {"textDocument/implementation", GotoDefinition(*this) }, - {"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)}, {"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)}, }, + m_client{_transport}, + m_handlers{m_coreHandlers}, m_fileRepository("/" /* basePath */, {} /* no search paths */), m_compilerStack{m_fileRepository.reader()} { } +void LanguageServer::registerHandler(std::string _name, MessageHandler _handler) +{ + m_handlers[std::move(_name)] = std::move(_handler); +} + Json::Value LanguageServer::toRange(SourceLocation const& _location) { return HandlerBase(*this).toRange(_location); @@ -409,14 +461,23 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) Json::Value replyArgs; replyArgs["serverInfo"]["name"] = "solc"; replyArgs["serverInfo"]["version"] = string(VersionNumber); - replyArgs["capabilities"]["definitionProvider"] = true; - replyArgs["capabilities"]["implementationProvider"] = true; replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; - replyArgs["capabilities"]["semanticTokensProvider"]["legend"] = semanticTokensLegend(); - replyArgs["capabilities"]["semanticTokensProvider"]["range"] = false; - replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true - replyArgs["capabilities"]["renameProvider"] = true; + + m_handlers = m_coreHandlers; + m_handlers["textDocument/didOpen"] = bind(&LanguageServer::handleTextDocumentDidOpen, this, _2); + m_handlers["textDocument/didChange"] = bind(&LanguageServer::handleTextDocumentDidChange, this, _2); + m_handlers["textDocument/didClose"] = bind(&LanguageServer::handleTextDocumentDidClose, this, _2); + m_handlers["workspace/didChangeConfiguration"] = bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2); + + for (unique_ptr& handler: m_onDemandHandlers) + { + Json::Value replyCapabilities = handler->onReportCapabilities(_args["capabilities"]); + if (!replyCapabilities.isObject()) + continue; + for (Json::String const& memberName: replyCapabilities.getMemberNames()) + replyArgs["capabilities"][memberName] = replyCapabilities[memberName]; + } m_client.reply(_id, std::move(replyArgs)); } @@ -427,23 +488,6 @@ void LanguageServer::handleInitialized(MessageID, Json::Value const&) compileAndUpdateDiagnostics(); } -void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args) -{ - auto uri = _args["textDocument"]["uri"]; - - compile(); - - auto const sourceName = m_fileRepository.uriToSourceUnitName(uri.as()); - SourceUnit const& ast = m_compilerStack.ast(sourceName); - m_compilerStack.charStream(sourceName); - Json::Value data = SemanticTokensBuilder().build(ast, m_compilerStack.charStream(sourceName)); - - Json::Value reply = Json::objectValue; - reply["data"] = data; - - m_client.reply(_id, std::move(reply)); -} - void LanguageServer::handleWorkspaceDidChangeConfiguration(Json::Value const& _args) { requireServerInitialized(); diff --git a/libsolidity/lsp/LanguageServer.h b/libsolidity/lsp/LanguageServer.h index a05bec497..75dd47562 100644 --- a/libsolidity/lsp/LanguageServer.h +++ b/libsolidity/lsp/LanguageServer.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include #include @@ -26,6 +27,7 @@ #include #include +#include #include #include #include @@ -80,6 +82,13 @@ public: frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos); frontend::CompilerStack const& compilerStack() const noexcept { return m_compilerStack; } + using MessageHandler = std::function; + + void registerHandler(std::string name, MessageHandler handler); + + /// Compile everything until after analysis phase. + void compile(); + 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. @@ -93,18 +102,12 @@ private: void handleTextDocumentDidClose(Json::Value const& _args); void handleRename(Json::Value const& _args); void handleGotoDefinition(MessageID _id, Json::Value const& _args); - void semanticTokensFull(MessageID _id, Json::Value const& _args); /// Invoked when the server user-supplied configuration changes (initiated by the client). void changeConfiguration(Json::Value const&); - /// Compile everything until after analysis phase. - void compile(); - std::vector allSolidityFilesFromProject() const; - using MessageHandler = std::function; - Json::Value toRange(langutil::SourceLocation const& _location); Json::Value toJson(langutil::SourceLocation const& _location); @@ -113,6 +116,11 @@ private: enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown }; State m_state = State::Started; + // Contains the list of features that are only advertised if advertised by the client + // during initialization stage. + std::array, 3> m_onDemandHandlers; + std::map m_coreHandlers; + Transport& m_client; std::map m_handlers; diff --git a/libsolidity/lsp/RenameSymbol.cpp b/libsolidity/lsp/RenameSymbol.cpp index e205beb73..1a3e2773b 100644 --- a/libsolidity/lsp/RenameSymbol.cpp +++ b/libsolidity/lsp/RenameSymbol.cpp @@ -16,6 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 #include +#include #include #include @@ -48,6 +49,19 @@ CallableDeclaration const* extractCallableDeclaration(FunctionCall const& _funct } +Json::Value RenameSymbol::onReportCapabilities(Json::Value const& _clientCapabilities) +{ + Json::Value replyCapabilities = Json::objectValue; + + if (_clientCapabilities["textDocument"]["rename"]) + { + replyCapabilities["renameProvider"] = true; + m_server.registerHandler("textDocument/rename", *this); + } + + return replyCapabilities; +} + void RenameSymbol::operator()(MessageID _id, Json::Value const& _args) { auto const&& [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args); diff --git a/libsolidity/lsp/RenameSymbol.h b/libsolidity/lsp/RenameSymbol.h index cafa8c067..e743c6864 100644 --- a/libsolidity/lsp/RenameSymbol.h +++ b/libsolidity/lsp/RenameSymbol.h @@ -16,6 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 #include +#include #include #include @@ -27,6 +28,8 @@ class RenameSymbol: public HandlerBase public: explicit RenameSymbol(LanguageServer& _server): HandlerBase(_server) {} + Json::Value onReportCapabilities(Json::Value const& _clientCapabilities) override; + void operator()(MessageID, Json::Value const&); protected: // Nested class because otherwise `RenameSymbol` couldn't be easily used diff --git a/test/lsp.py b/test/lsp.py index 2e097dab0..f184f6baf 100755 --- a/test/lsp.py +++ b/test/lsp.py @@ -927,7 +927,13 @@ class SolidityLSPTestSuite: # {{{ 'trace': 'messages', 'capabilities': { 'textDocument': { - 'publishDiagnostics': {'relatedInformation': True} + 'definition': {'dynamicRegistration': True}, + 'documentHighlight': {'dynamicRegistration': True}, + 'implementation': {'dynamicRegistration': True}, + 'publishDiagnostics': {'relatedInformation': True}, + 'references': {'dynamicRegistration': True}, + 'rename': {'dynamicRegistration': True}, + 'semanticTokens': {'dynamicRegistration': True} }, 'workspace': { 'applyEdit': True,