Language Server: Constrain server feature set based on advertised client capabilities.

This commit is contained in:
Christian Parpart 2022-10-12 16:57:11 +02:00
parent 2cb618a5c3
commit 5432c5cdb0
10 changed files with 161 additions and 37 deletions

View File

@ -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:

View File

@ -16,6 +16,7 @@
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/GotoDefinition.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/Transport.h> // for RequestError
#include <libsolidity/lsp/Utils.h>
#include <libsolidity/ast/AST.h>
@ -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);

View File

@ -16,6 +16,7 @@
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/HandlerBase.h>
#include <libsolidity/lsp/Transport.h>
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&);
};

View File

@ -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())

View File

@ -18,17 +18,19 @@
#pragma once
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <liblangutil/SourceLocation.h>
#include <liblangutil/CharStreamProvider.h>
#include <libsolutil/JSON.h>
#include <optional>
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<std::string, langutil::LineColumn> 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;

View File

@ -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<string>());
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<GotoDefinition>(*this),
make_unique<RenameSymbol>(*this),
make_unique<SemanticTokensHandler>(*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<HandlerBase>& 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<string>());
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();

View File

@ -18,6 +18,7 @@
#pragma once
#include <libsolidity/lsp/Transport.h>
#include <libsolidity/lsp/HandlerBase.h>
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/interface/CompilerStack.h>
#include <libsolidity/interface/FileReader.h>
@ -26,6 +27,7 @@
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@ -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(MessageID, Json::Value const&)>;
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<boost::filesystem::path> allSolidityFilesFromProject() const;
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
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<std::unique_ptr<HandlerBase>, 3> m_onDemandHandlers;
std::map<std::string, MessageHandler> m_coreHandlers;
Transport& m_client;
std::map<std::string, MessageHandler> m_handlers;

View File

@ -16,6 +16,7 @@
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/RenameSymbol.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/Utils.h>
#include <libyul/AST.h>
@ -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);

View File

@ -16,6 +16,7 @@
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/HandlerBase.h>
#include <libsolidity/lsp/Transport.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTVisitor.h>
@ -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

View File

@ -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,