Adds initial support for the Language Server Protocol (via --lsp CLI option).

This commit is contained in:
Christian Parpart 2021-05-17 15:43:06 +02:00 committed by chriseth
parent 7f3e3584fc
commit 702620151f
27 changed files with 2607 additions and 12 deletions

View File

@ -198,6 +198,23 @@ defaults:
- store_artifacts: *artifacts_test_results
- gitter_notify_failure_unless_pr
- steps_test_lsp: &steps_test_lsp
steps:
- checkout
- attach_workspace:
at: build
- run:
name: Install dependencies
command: |
pip install --user git+https://github.com/christianparpart/pylspclient.git
pip install --user deepdiff pytest
- run:
name: LSP_test
command: ./test/libsolidity/lsp/solc-lsp-test.py
- store_test_results: *store_test_results
- store_artifacts: *artifacts_test_results
- gitter_notify_failure_unless_pr
- steps_soltest_all: &steps_soltest_all
steps:
- checkout
@ -458,7 +475,9 @@ jobs:
command: apt -q update && apt install -y python3-pip
- run:
name: Install pylint
command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate
command: |
python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate
pip install --user git+https://github.com/christianparpart/pylspclient.git deepdiff pytest
# also z3-solver, parsec and tabulate to make sure pylint knows about this module, pygments-lexer-solidity for docs
- run:
name: Linting Python Scripts
@ -810,6 +829,11 @@ jobs:
parallelism: 6
<<: *steps_soltest_all
t_ubu_lsp: &t_ubu_lsp
<<: *base_ubuntu2004
parallelism: 6
<<: *steps_test_lsp
t_archlinux_soltest: &t_archlinux_soltest
<<: *base_archlinux
environment:
@ -1189,6 +1213,7 @@ workflows:
- t_ubu_soltest_enforce_yul: *workflow_ubuntu2004
- b_ubu_clang: *workflow_trigger_on_tags
- t_ubu_clang_soltest: *workflow_ubuntu2004_clang
- t_ubu_lsp: *workflow_ubuntu2004
# Ubuntu fake release build and tests
- b_ubu_release: *workflow_trigger_on_tags

View File

@ -138,4 +138,10 @@ endif()
if (TESTS AND NOT EMSCRIPTEN)
add_subdirectory(test)
add_custom_target(lsptest
COMMAND ./test/libsolidity/lsp/solc-lsp-test.py
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
DEPENDS solc
)
endif()

View File

@ -206,7 +206,7 @@ public:
return nullptr;
}
static Severity errorSeverity(Type _type)
static constexpr Severity errorSeverity(Type _type)
{
if (_type == Type::Info)
return Severity::Info;

View File

@ -155,6 +155,14 @@ set(sources
interface/StorageLayout.h
interface/Version.cpp
interface/Version.h
lsp/LanguageServer.cpp
lsp/LanguageServer.h
lsp/ReferenceCollector.cpp
lsp/ReferenceCollector.h
lsp/SemanticTokensBuilder.cpp
lsp/SemanticTokensBuilder.h
lsp/Transport.cpp
lsp/Transport.h
parsing/DocStringParser.cpp
parsing/DocStringParser.h
parsing/Parser.cpp

View File

@ -45,7 +45,7 @@ namespace solidity::frontend
FileReader::FileReader(
boost::filesystem::path _basePath,
vector<boost::filesystem::path> const& _includePaths,
vector<boost::filesystem::path> _includePaths,
FileSystemPathSet _allowedDirectories
):
m_allowedDirectories(std::move(_allowedDirectories)),
@ -172,7 +172,7 @@ ReadCallback::Result FileReader::readFile(string const& _kind, string const& _so
}
}
string FileReader::cliPathToSourceUnitName(boost::filesystem::path const& _cliPath)
string FileReader::cliPathToSourceUnitName(boost::filesystem::path const& _cliPath) const
{
vector<boost::filesystem::path> prefixes = {m_basePath.empty() ? normalizeCLIPathForVFS(".") : m_basePath};
prefixes += m_includePaths;

View File

@ -48,7 +48,7 @@ public:
/// that will be used when requesting files from this file reader instance.
explicit FileReader(
boost::filesystem::path _basePath = {},
std::vector<boost::filesystem::path> const& _includePaths = {},
std::vector<boost::filesystem::path> _includePaths = {},
FileSystemPathSet _allowedDirectories = {}
);
@ -94,7 +94,7 @@ public:
/// Creates a source unit name by normalizing a path given on the command line and, if possible,
/// making it relative to base path or one of the include directories.
std::string cliPathToSourceUnitName(boost::filesystem::path const& _cliPath);
std::string cliPathToSourceUnitName(boost::filesystem::path const& _cliPath) const;
/// Checks if a set contains any paths that lead to different files but would receive identical
/// source unit names. Files are considered the same if their paths are exactly the same after

View File

@ -0,0 +1,70 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <liblangutil/SourceLocation.h>
#include <liblangutil/SourceReferenceExtractor.h> // LineColumn
#include <optional>
#include <ostream>
#include <string>
#include <vector>
namespace solidity::lsp
{
struct LineColumnRange
{
langutil::LineColumn start;
langutil::LineColumn end;
};
struct DocumentPosition
{
/// Path in lsp client notation.
std::string path;
langutil::LineColumn position;
};
enum class DocumentHighlightKind
{
Unspecified,
Text, //!< a textual occurrence
Read, //!< read access to a variable
Write, //!< write access to a variable
};
// Represents a symbol / AST node that is to be highlighted, with some context associated.
struct DocumentHighlight
{
langutil::SourceLocation location;
// std::string sourceName;
// LineColumnRange range;
DocumentHighlightKind kind = DocumentHighlightKind::Unspecified;
};
/// Represents a related message and source code location for a diagnostic. This should be
/// used to point to code locations that cause or related to a diagnostics, e.g when duplicating
/// a symbol in a scope.
struct DiagnosticRelatedInformation
{
langutil::SourceLocation location; // The location of this related diagnostic information.
std::string message; // The message of this related diagnostic information.
};
}

View File

@ -0,0 +1,740 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTUtils.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <libsolidity/interface/ReadFile.h>
#include <libsolidity/interface/StandardCompiler.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/ReferenceCollector.h>
#include <libsolidity/lsp/SemanticTokensBuilder.h>
#include <liblangutil/SourceReferenceExtractor.h>
#include <liblangutil/CharStream.h>
#include <libsolutil/Visitor.h>
#include <libsolutil/JSON.h>
#include <boost/filesystem.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <fmt/format.h>
#include <ostream>
#include <string>
using namespace std;
using namespace std::placeholders;
using namespace solidity::lsp;
using namespace solidity::langutil;
using namespace solidity::frontend;
namespace
{
void log(string const& _message)
{
#if 0
static ofstream logFile("/tmp/solc.lsp.log", std::ios::app);
logFile << _message << endl;
#else
(void)_message;
#endif
}
struct MarkdownBuilder
{
std::stringstream result;
MarkdownBuilder& code(std::string const& _code)
{
// TODO: Use solidity as language Id as soon as possible.
auto constexpr SolidityLanguageId = "javascript";
result << "```" << SolidityLanguageId << '\n' << _code << "\n```\n\n";
return *this;
}
MarkdownBuilder& text(std::string const& _text)
{
if (_text.empty())
{
result << _text << '\n';
if (_text.back() != '\n') // We want double-LF to ensure constructing a paragraph.
result << '\n';
}
return *this;
}
};
Json::Value toJson(LineColumn _pos)
{
Json::Value json = Json::objectValue;
json["line"] = max(_pos.line, 0);
json["character"] = max(_pos.column, 0);
return json;
}
Json::Value toJsonRange(int _startLine, int _startColumn, int _endLine, int _endColumn)
{
Json::Value json;
json["start"] = toJson({_startLine, _startColumn});
json["end"] = toJson({_endLine, _endColumn});
return json;
}
constexpr int toDiagnosticSeverity(Error::Type _errorType)
{
// 1=Error, 2=Warning, 3=Info, 4=Hint
switch (Error::errorSeverity(_errorType))
{
case Error::Severity::Error: return 1;
case Error::Severity::Warning: return 2;
case Error::Severity::Info: return 3;
}
return 1;
}
vector<Declaration const*> allAnnotatedDeclarations(Identifier const* _identifier)
{
vector<Declaration const*> output;
output.push_back(_identifier->annotation().referencedDeclaration);
output += _identifier->annotation().candidateDeclarations;
return output;
}
Json::Value semanticTokensLegend()
{
Json::Value legend = Json::objectValue;
// NOTE! The (alphabetical) order and items must match exactly the items of
// their respective enum class members.
Json::Value tokenTypes = Json::arrayValue;
tokenTypes.append("class");
tokenTypes.append("comment");
tokenTypes.append("enum");
tokenTypes.append("enumMember");
tokenTypes.append("event");
tokenTypes.append("function");
tokenTypes.append("interface");
tokenTypes.append("keyword");
tokenTypes.append("macro");
tokenTypes.append("method");
tokenTypes.append("modifier");
tokenTypes.append("number");
tokenTypes.append("operator");
tokenTypes.append("parameter");
tokenTypes.append("property");
tokenTypes.append("string");
tokenTypes.append("struct");
tokenTypes.append("type");
tokenTypes.append("typeParameter");
tokenTypes.append("variable");
legend["tokenTypes"] = tokenTypes;
Json::Value tokenModifiers = Json::arrayValue;
tokenModifiers.append("abstract");
tokenModifiers.append("declaration");
tokenModifiers.append("definition");
tokenModifiers.append("deprecated");
tokenModifiers.append("documentation");
tokenModifiers.append("modification");
tokenModifiers.append("readonly");
legend["tokenModifiers"] = tokenModifiers;
return legend;
}
}
LanguageServer::LanguageServer(unique_ptr<Transport> _transport):
m_client{move(_transport)},
m_handlers{
{"$/cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */} },
{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */} },
{"exit", [this](auto, auto) { terminate(); }},
{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)},
{"initialized", [](auto, auto) {} },
{"shutdown", [this](auto, auto) { m_shutdownRequested = true; }},
{"textDocument/definition", [this](auto _id, auto _args) { handleGotoDefinition(_id, _args); }},
{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _1, _2)},
{"textDocument/didClose", [](auto, auto) {/*nothing for now*/}},
{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _1, _2)},
{"textDocument/documentHighlight", bind(&LanguageServer::handleTextDocumentHighlight, this, _1, _2)},
{"textDocument/hover", bind(&LanguageServer::handleTextDocumentHover, this, _1, _2)},
{"textDocument/implementation", [this](auto _id, auto _args) { handleGotoDefinition(_id, _args); }},
{"textDocument/references", bind(&LanguageServer::handleTextDocumentReferences, this, _1, _2)},
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _1, _2)},
},
m_fileReader{"/"},
m_compilerStack{bind(&FileReader::readFile, ref(m_fileReader), _1, _2)}
{
}
DocumentPosition LanguageServer::extractDocumentPosition(Json::Value const& _json) const
{
DocumentPosition dpos{};
dpos.path = _json["textDocument"]["uri"].asString();
dpos.position.line = _json["position"]["line"].asInt();
dpos.position.column = _json["position"]["character"].asInt();
return dpos;
}
Json::Value LanguageServer::toRange(SourceLocation const& _location) const
{
solAssert(_location.sourceName, "");
CharStream const& stream = m_compilerStack.charStream(*_location.sourceName);
auto const [startLine, startColumn] = stream.translatePositionToLineColumn(_location.start);
auto const [endLine, endColumn] = stream.translatePositionToLineColumn(_location.end);
return toJsonRange(startLine, startColumn, endLine, endColumn);
}
Json::Value LanguageServer::toJson(SourceLocation const& _location) const
{
solAssert(_location.sourceName);
Json::Value item = Json::objectValue;
item["uri"] = m_fileMappings.at(*_location.sourceName);
item["range"] = toRange(_location);
return item;
}
string LanguageServer::clientPathToSourceUnitName(string const& _path) const
{
return m_fileReader.cliPathToSourceUnitName(_path);
}
bool LanguageServer::clientPathSourceKnown(string const& _path) const
{
return m_fileReader.sourceCodes().count(clientPathToSourceUnitName(_path));
}
void LanguageServer::changeConfiguration(Json::Value const& _settings)
{
m_settingsObject = _settings;
}
bool LanguageServer::compile(string const& _path)
{
// TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty).
if (!clientPathSourceKnown(_path))
return false;
m_compilerStack.reset(false);
m_compilerStack.setSources(m_fileReader.sourceCodes());
m_compilerStack.compile(CompilerStack::State::AnalysisPerformed);
return true;
}
void LanguageServer::compileSourceAndReport(string const& _path)
{
compile(_path);
Json::Value params;
params["uri"] = _path;
params["diagnostics"] = Json::arrayValue;
for (shared_ptr<Error const> const& error: m_compilerStack.errors())
{
SourceReferenceExtractor::Message const message = SourceReferenceExtractor::extract(m_compilerStack, *error);
Json::Value jsonDiag;
jsonDiag["source"] = "solc";
jsonDiag["severity"] = toDiagnosticSeverity(error->type());
jsonDiag["message"] = message.primary.message;
jsonDiag["range"] = toJsonRange(
message.primary.position.line, message.primary.startColumn,
message.primary.position.line, message.primary.endColumn
);
if (message.errorId.has_value())
jsonDiag["code"] = Json::UInt64{message.errorId.value().error};
for (SourceReference const& secondary: message.secondary)
{
Json::Value jsonRelated;
jsonRelated["message"] = secondary.message;
// TODO translate back?
jsonRelated["location"]["uri"] = secondary.sourceName;
jsonRelated["location"]["range"] = toJsonRange(
secondary.position.line, secondary.startColumn,
secondary.position.line, secondary.endColumn
);
jsonDiag["relatedInformation"].append(jsonRelated);
}
params["diagnostics"].append(jsonDiag);
}
m_client->notify("textDocument/publishDiagnostics", params);
}
ASTNode const* LanguageServer::requestASTNode(DocumentPosition _filePos)
{
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)
return nullptr;
if (!clientPathSourceKnown(_filePos.path))
return nullptr;
string sourceUnitName = clientPathToSourceUnitName(_filePos.path);
optional<int> sourcePos = m_compilerStack.charStream(sourceUnitName)
.translateLineColumnToPosition(_filePos.position.line, _filePos.position.column);
if (!sourcePos.has_value())
return nullptr;
return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(sourceUnitName));
}
optional<SourceLocation> LanguageServer::declarationPosition(Declaration const* _declaration)
{
if (!_declaration)
return nullopt;
if (_declaration->nameLocation().isValid())
return _declaration->nameLocation();
if (_declaration->location().isValid())
return _declaration->location();
return nullopt;
}
vector<SourceLocation> LanguageServer::findAllReferences(
Declaration const* _declaration,
string const& _sourceIdentifierName,
SourceUnit const& _sourceUnit
)
{
vector<SourceLocation> output;
for (DocumentHighlight& highlight: ReferenceCollector::collect(_declaration, _sourceUnit, _sourceIdentifierName))
output.emplace_back(move(highlight.location));
return output;
}
void LanguageServer::findAllReferences(
Declaration const* _declaration,
string const& _sourceIdentifierName,
SourceUnit const& _sourceUnit,
vector<SourceLocation>& _output
)
{
for (DocumentHighlight& highlight: ReferenceCollector::collect(_declaration, _sourceUnit, _sourceIdentifierName))
_output.emplace_back(move(highlight.location));
}
vector<SourceLocation> LanguageServer::references(DocumentPosition _documentPosition)
{
ASTNode const* sourceNode = requestASTNode(_documentPosition);
if (!sourceNode)
return {};
SourceUnit const& sourceUnit = m_compilerStack.ast(clientPathToSourceUnitName(_documentPosition.path));
vector<SourceLocation> output;
if (auto const* identifier = dynamic_cast<Identifier const*>(sourceNode))
{
for (auto const* declaration: allAnnotatedDeclarations(identifier))
output += findAllReferences(declaration, declaration->name(), sourceUnit);
}
else if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
{
output += findAllReferences(declaration, declaration->name(), sourceUnit);
}
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(sourceNode))
{
if (Declaration const* decl = memberAccess->annotation().referencedDeclaration)
output += findAllReferences(decl, memberAccess->memberName(), sourceUnit);
}
return output;
}
vector<DocumentHighlight> LanguageServer::semanticHighlight(ASTNode const* _sourceNode, string const& _path)
{
ASTNode const* sourceNode = _sourceNode; // TODO
if (!sourceNode)
return {};
SourceUnit const& sourceUnit = m_compilerStack.ast(clientPathToSourceUnitName(_path));
vector<DocumentHighlight> output;
if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
{
output += ReferenceCollector::collect(declaration, sourceUnit, declaration->name());
}
else if (auto const* identifier = dynamic_cast<Identifier const*>(sourceNode))
{
for (auto const* declaration: allAnnotatedDeclarations(identifier))
output += ReferenceCollector::collect(declaration, sourceUnit, identifier->name());
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(sourceNode))
{
solAssert(!identifierPath->path().empty(), "");
output += ReferenceCollector::collect(identifierPath->annotation().referencedDeclaration, sourceUnit, identifierPath->path().back());
}
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(sourceNode))
{
Type const* type = memberAccess->expression().annotation().type;
if (auto const* ttype = dynamic_cast<TypeType const*>(type))
{
auto const memberName = memberAccess->memberName();
if (auto const* enumType = dynamic_cast<EnumType const*>(ttype->actualType()))
{
// find the definition
vector<DocumentHighlight> output;
for (ASTPointer<EnumValue> const& enumMember: enumType->enumDefinition().members())
if (enumMember->name() == memberName)
output += ReferenceCollector::collect(enumMember.get(), sourceUnit, enumMember->name());
// TODO: find uses of the enum value
}
}
else if (auto const* structType = dynamic_cast<StructType const*>(type))
{
(void) structType; // TODO
// TODO: highlight all struct member occurrences.
// memberAccess->memberName()
// structType->
}
else
{
// TODO: EnumType, ...
//log("semanticHighlight: member type is: "s + (type ? typeid(*type).name() : "NULL"));
}
}
return output;
}
bool LanguageServer::run()
{
while (!m_exitRequested && !m_client->closed())
{
optional<Json::Value> const jsonMessage = m_client->receive();
if (!jsonMessage)
continue;
try
{
string const methodName = (*jsonMessage)["method"].asString();
MessageID const id = (*jsonMessage)["id"];
if (auto handler = valueOrDefault(m_handlers, methodName))
handler(id, (*jsonMessage)["params"]);
else
m_client->error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName);
}
catch (exception const& e)
{
log("Unhandled exception caught when handling message. "s + e.what());
}
}
return m_shutdownRequested;
}
void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
{
// The default of FileReader is to use `.`, but the path from where the LSP was started
// should not matter.
string rootPath("/");
if (Json::Value uri = _args["rootUri"])
rootPath = uri.asString();
else if (Json::Value rootPath = _args["rootPath"])
rootPath = rootPath.asString();
//log("root path: " + rootPath);
m_fileReader.setBasePath(boost::filesystem::path(rootPath));
if (_args["initializationOptions"].isObject())
changeConfiguration(_args["initializationOptions"]);
Json::Value replyArgs;
replyArgs["serverInfo"]["name"] = "solc";
replyArgs["serverInfo"]["version"] = string(VersionNumber);
replyArgs["hoverProvider"] = true;
replyArgs["capabilities"]["hoverProvider"] = true;
replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true;
replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental
replyArgs["capabilities"]["definitionProvider"] = true;
replyArgs["capabilities"]["implementationProvider"] = true;
replyArgs["capabilities"]["documentHighlightProvider"] = true;
replyArgs["capabilities"]["referencesProvider"] = true;
replyArgs["capabilities"]["semanticTokensProvider"]["legend"] = semanticTokensLegend();
replyArgs["capabilities"]["semanticTokensProvider"]["range"] = true;
replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
m_client->reply(_id, replyArgs);
}
void LanguageServer::handleWorkspaceDidChangeConfiguration(MessageID, Json::Value const& _args)
{
if (_args["settings"].isObject())
changeConfiguration(_args["settings"]);
}
void LanguageServer::handleTextDocumentDidOpen(MessageID /*_id*/, Json::Value const& _args)
{
if (!_args["textDocument"])
return;
auto const text = _args["textDocument"]["text"].asString();
auto uri = _args["textDocument"]["uri"].asString();
m_fileMappings[clientPathToSourceUnitName(uri)] = uri;
m_fileReader.setSource(uri, text);
compileSourceAndReport(uri);
}
void LanguageServer::handleTextDocumentDidChange(MessageID /*_id*/, Json::Value const& _args)
{
auto const uri = _args["textDocument"]["uri"].asString();
auto const contentChanges = _args["contentChanges"];
for (Json::Value jsonContentChange: contentChanges)
{
if (!jsonContentChange.isObject()) // Protocol error, will only happen on broken clients, so silently ignore it.
continue;
if (!clientPathSourceKnown(uri))
// should be an error as well
continue;
string text = jsonContentChange["text"].asString();
if (!jsonContentChange["range"].isObject()) // full content update
{
m_fileReader.setSource(uri, move(text));
continue;
}
Json::Value const jsonRange = jsonContentChange["range"];
// TODO could use a general helper to read line/characer json objects into int pairs or whateveer
int const startLine = jsonRange["start"]["line"].asInt();
int const startColumn = jsonRange["start"]["character"].asInt();
int const endLine = jsonRange["end"]["line"].asInt();
int const endColumn = jsonRange["end"]["character"].asInt();
string buffer = m_fileReader.sourceCodes().at(clientPathToSourceUnitName(uri));
optional<int> const startOpt = CharStream::translateLineColumnToPosition(buffer, startLine, startColumn);
optional<int> const endOpt = CharStream::translateLineColumnToPosition(buffer, endLine, endColumn);
if (!startOpt || !endOpt)
continue;
size_t const start = static_cast<size_t>(startOpt.value());
size_t const count = static_cast<size_t>(endOpt.value()) - start; // TODO: maybe off-by-1 bug? +1 missing?
buffer.replace(start, count, move(text));
m_fileReader.setSource(uri, move(buffer));
}
if (!contentChanges.empty())
compileSourceAndReport(uri);
}
void LanguageServer::handleGotoDefinition(MessageID _id, Json::Value const& _args)
{
ASTNode const* sourceNode = requestASTNode(extractDocumentPosition(_args));
vector<SourceLocation> locations;
if (auto const* identifier = dynamic_cast<Identifier const*>(sourceNode))
{
for (auto const* declaration: allAnnotatedDeclarations(identifier))
if (auto location = declarationPosition(declaration); location.has_value())
locations.emplace_back(move(location.value()));
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(sourceNode))
{
if (auto const* declaration = identifierPath->annotation().referencedDeclaration)
if (auto location = declarationPosition(declaration); location.has_value())
locations.emplace_back(move(location.value()));
}
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(sourceNode))
{
auto const location = declarationPosition(memberAccess->annotation().referencedDeclaration);
if (location.has_value())
locations.emplace_back(location.value());
}
else if (auto const* importDirective = dynamic_cast<ImportDirective const*>(sourceNode))
{
auto const& path = *importDirective->annotation().absolutePath;
if (m_fileReader.sourceCodes().count(path))
locations.emplace_back(SourceLocation{0, 0, make_shared<string const>(path)});
}
else if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
{
if (auto location = declarationPosition(declaration); location.has_value())
locations.emplace_back(move(location.value()));
}
else if (sourceNode)
{
log(fmt::format("Could not infer def of {}", typeid(*sourceNode).name()));
}
Json::Value reply = Json::arrayValue;
for (SourceLocation const& location: locations)
reply.append(toJson(location));
m_client->reply(_id, reply);
}
string LanguageServer::symbolHoverInformation(ASTNode const* _sourceNode)
{
MarkdownBuilder markdown{};
// Try getting the type definition of the underlying AST node, if available.
if (auto const* expression = dynamic_cast<Expression const*>(_sourceNode))
{
if (expression->annotation().type)
markdown.code(expression->annotation().type->toString(false));
if (auto const* declaration = ASTNode::referencedDeclaration(*expression))
if (declaration->type())
markdown.code(declaration->type()->toString(false));
}
else if (auto const* declaration = dynamic_cast<Declaration const*>(_sourceNode))
{
if (declaration->type())
markdown.code(declaration->type()->toString(false));
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(_sourceNode))
{
Declaration const* decl = identifierPath->annotation().referencedDeclaration;
if (decl && decl->type())
markdown.code(decl->type()->toString(false));
if (auto const* node = dynamic_cast<StructurallyDocumented const*>(decl))
if (node->documentation()->text())
markdown.text(*node->documentation()->text());
}
else if (auto const* expression = dynamic_cast<Expression const*>(_sourceNode))
{
if (auto const* declaration = ASTNode::referencedDeclaration(*expression))
if (declaration->type())
markdown.code(declaration->type()->toString(false));
}
else
{
markdown.text(fmt::format("Unhandled AST node type in hover: {}\n", typeid(*_sourceNode).name()));
log(fmt::format("Unhandled AST node type in hover: {}\n", typeid(*_sourceNode).name()));
}
// If this AST node contains documentation itself, append it.
if (auto const* documented = dynamic_cast<StructurallyDocumented const*>(_sourceNode))
{
if (documented->documentation())
markdown.text(*documented->documentation()->text());
}
return markdown.result.str();
}
void LanguageServer::handleTextDocumentHover(MessageID _id, Json::Value const& _args)
{
auto const sourceNode = requestASTNode(extractDocumentPosition(_args));
string tooltipText = symbolHoverInformation(sourceNode);
if (tooltipText.empty())
{
m_client->reply(_id, Json::nullValue);
return;
}
Json::Value reply = Json::objectValue;
reply["range"] = toRange(sourceNode->location());
reply["contents"]["kind"] = "markdown";
reply["contents"]["value"] = move(tooltipText);
m_client->reply(_id, reply);
}
void LanguageServer::handleTextDocumentHighlight(MessageID _id, Json::Value const& _args)
{
auto const dpos = extractDocumentPosition(_args);
ASTNode const* sourceNode = requestASTNode(dpos);
Json::Value jsonReply = Json::arrayValue;
for (DocumentHighlight const& highlight: semanticHighlight(sourceNode, dpos.path))
{
Json::Value item = Json::objectValue;
item["range"] = toRange(highlight.location);
if (highlight.kind != DocumentHighlightKind::Unspecified)
item["kind"] = int(highlight.kind);
jsonReply.append(item);
}
m_client->reply(_id, jsonReply);
}
void LanguageServer::handleTextDocumentReferences(MessageID _id, Json::Value const& _args)
{
auto const dpos = extractDocumentPosition(_args);
auto const sourceNode = requestASTNode(dpos);
if (!sourceNode)
{
Json::Value emptyResponse = Json::arrayValue;
m_client->reply(_id, emptyResponse); // reply with "No references".
return;
}
string sourceUnitName = clientPathToSourceUnitName(dpos.path);
SourceUnit const& sourceUnit = m_compilerStack.ast(sourceUnitName);
auto output = vector<SourceLocation>{};
if (auto const* identifier = dynamic_cast<Identifier const*>(sourceNode))
{
for (auto const* declaration: allAnnotatedDeclarations(identifier))
output += findAllReferences(declaration, declaration->name(), sourceUnit);
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(sourceNode))
{
if (auto decl = identifierPath->annotation().referencedDeclaration)
output += findAllReferences(decl, decl->name(), sourceUnit);
}
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(sourceNode))
{
output += findAllReferences(memberAccess->annotation().referencedDeclaration, memberAccess->memberName(), sourceUnit);
}
else if (auto const* declaration = dynamic_cast<Declaration const*>(sourceNode))
{
output += findAllReferences(declaration, declaration->name(), sourceUnit);
}
Json::Value jsonReply = Json::arrayValue;
for (SourceLocation const& location: output)
jsonReply.append(toJson(location));
log("Sending reply");
m_client->reply(_id, jsonReply);
}
void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args)
{
auto uri = _args["textDocument"]["uri"];
if (!compile(uri.as<string>()))
return;
auto const sourceName = clientPathToSourceUnitName(uri.as<string>());
SourceUnit const& ast = m_compilerStack.ast(sourceName);
m_compilerStack.charStream(sourceName);
SemanticTokensBuilder semanticTokensBuilder;
Json::Value data = semanticTokensBuilder.build(ast, m_compilerStack.charStream(sourceName));
Json::Value reply = Json::objectValue;
reply["data"] = data;
m_client->reply(_id, reply);
}
void LanguageServer::terminate()
{
m_exitRequested = true;
}

View File

@ -0,0 +1,164 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/ast/AST.h>
#include <libsolidity/formal/ModelCheckerSettings.h>
#include <libsolidity/interface/CompilerStack.h>
#include <libsolidity/interface/StandardCompiler.h>
#include <libsolidity/interface/FileReader.h>
#include <libsolidity/interface/ImportRemapper.h>
#include <libsolidity/lsp/LSPTypes.h>
#include <libsolidity/lsp/ReferenceCollector.h>
#include <libsolidity/lsp/Transport.h>
#include <liblangutil/SourceReferenceExtractor.h>
#include <fmt/format.h>
#include <json/value.h>
#include <boost/filesystem/path.hpp>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <optional>
#include <ostream>
#include <set>
#include <string>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector>
namespace solidity::lsp
{
enum class ErrorCode;
/// Solidity Language Server, managing one LSP client.
///
/// This implements a subset of LSP version 3.16 that can be found at:
/// https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/
class LanguageServer
{
public:
/// @param _transport Customizable transport layer.
explicit LanguageServer(std::unique_ptr<Transport> _transport);
/// Compiles the source behind path @p _file and updates the diagnostics pushed to the client.
///
/// update diagnostics and also pushes any updates to the client.
void compileSourceAndReport(std::string const& _file);
/// Loops over incoming messages via the transport layer until shutdown condition is met.
///
/// The standard shutdown condition is when the maximum number of consecutive failures
/// has been exceeded.
///
/// @return boolean indicating normal or abnormal termination.
bool run();
/// Initiates the main event loop to terminate as soon as possible.
void terminate();
Transport& transport() noexcept { return *m_client; }
protected:
void handleInitialize(MessageID _id, Json::Value const& _args);
void handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args);
void handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args);
void handleTextDocumentDidChange(MessageID _id, Json::Value const& _args);
void handleTextDocumentHover(MessageID _id, Json::Value const& _args);
void handleTextDocumentHighlight(MessageID _id, Json::Value const& _args);
void handleTextDocumentReferences(MessageID _id, Json::Value const& _args);
void handleGotoDefinition(MessageID _id, Json::Value const& _args);
void semanticTokensFull(MessageID _id, Json::Value const& _args);
/**
* Constructs some tooltip (hover) text.
*
* The resulting text string should be in markdown format.
*/
std::string symbolHoverInformation(frontend::ASTNode const* _node);
/// Invoked when the server user-supplied configuration changes (initiated by the client).
void changeConfiguration(Json::Value const&);
/// Find all semantically equivalent occurrences of the symbol the current cursor is located at.
///
/// @returns a list of ranges to highlight as well as their use kind (read fraom, written to, other text).
std::vector<DocumentHighlight> semanticHighlight(frontend::ASTNode const* _node, std::string const& _path);
/// Finds all references of the current symbol at the given document position.
///
/// @returns all references as document ranges as well as their use kind (read fraom, written to, other text).
std::vector<langutil::SourceLocation> references(DocumentPosition _documentPosition);
/// Requests compilation of given client path.
/// @returns false if the file was not found.
bool compile(std::string const& _path);
frontend::ASTNode const* requestASTNode(DocumentPosition _filePos);
std::optional<langutil::SourceLocation> declarationPosition(frontend::Declaration const* _declaration);
std::vector<langutil::SourceLocation> findAllReferences(
frontend::Declaration const* _declaration,
std::string const& _sourceIdentifierName,
frontend::SourceUnit const& _sourceUnit
);
void findAllReferences(
frontend::Declaration const* _declaration,
std::string const& _sourceIdentifierName,
frontend::SourceUnit const& _sourceUnit,
std::vector<langutil::SourceLocation>& _output
);
DocumentPosition extractDocumentPosition(Json::Value const& _json) const;
Json::Value toRange(langutil::SourceLocation const& _location) const;
Json::Value toJson(langutil::SourceLocation const& _location) const;
/// Translates an LSP client path to the internal source unit name for the compiler.
std::string clientPathToSourceUnitName(std::string const& _path) const;
/// @returns true if we store the source for given the LSP client path.
bool clientPathSourceKnown(std::string const& _path) const;
// LSP related member fields
using Handler = std::function<void(MessageID, Json::Value const&)>;
using HandlerMap = std::unordered_map<std::string, Handler>;
std::unique_ptr<Transport> m_client;
HandlerMap m_handlers;
bool m_shutdownRequested = false;
bool m_exitRequested = false;
/// FileReader is used for reading files during comilation phase but is also used as VFS for the LSP.
frontend::FileReader m_fileReader;
/// Mapping from LSP client path to source unit name.
std::map<std::string, std::string> m_fileMappings;
frontend::CompilerStack m_compilerStack;
Json::Value m_settingsObject;
};
} // namespace solidity

View File

@ -0,0 +1,106 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/ReferenceCollector.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/lsp/LanguageServer.h>
using namespace solidity::frontend;
using namespace std::string_literals;
using namespace std;
namespace solidity::lsp
{
ReferenceCollector::ReferenceCollector(
frontend::Declaration const& _declaration,
std::string const& _sourceIdentifierName
):
m_declaration{_declaration},
m_sourceIdentifierName{_sourceIdentifierName.empty() ? _declaration.name() : _sourceIdentifierName}
{
}
std::vector<DocumentHighlight> ReferenceCollector::collect(
frontend::Declaration const* _declaration,
frontend::ASTNode const& _ast,
std::string const& _sourceIdentifierName
)
{
if (!_declaration)
return {};
// TODO if vardecl, just use decl's scope (for lower overhead).
ReferenceCollector collector(*_declaration, _sourceIdentifierName);
_ast.accept(collector);
return move(collector.m_result);
}
void ReferenceCollector::endVisit(frontend::ImportDirective const& _import)
{
for (auto const& symbolAlias: _import.symbolAliases())
if (m_sourceIdentifierName == *symbolAlias.alias)
{
m_result.emplace_back(DocumentHighlight{symbolAlias.location, DocumentHighlightKind::Text});
break;
}
}
bool ReferenceCollector::tryAddReference(frontend::Declaration const* _declaration, solidity::langutil::SourceLocation const& _location)
{
if (&m_declaration != _declaration)
return false;
m_result.emplace_back(DocumentHighlight{_location, DocumentHighlightKind::Text});
return true;
}
void ReferenceCollector::endVisit(frontend::Identifier const& _identifier)
{
if (auto const* declaration = _identifier.annotation().referencedDeclaration)
tryAddReference(declaration, _identifier.location());
for (auto const* declaration: _identifier.annotation().candidateDeclarations + _identifier.annotation().overloadedDeclarations)
tryAddReference(declaration, _identifier.location());
}
void ReferenceCollector::endVisit(frontend::IdentifierPath const& _identifierPath)
{
tryAddReference(_identifierPath.annotation().referencedDeclaration, _identifierPath.location());
}
void ReferenceCollector::endVisit(frontend::MemberAccess const& _memberAccess)
{
if (_memberAccess.annotation().referencedDeclaration == &m_declaration)
m_result.emplace_back(DocumentHighlight{_memberAccess.location(), DocumentHighlightKind::Text});
}
bool ReferenceCollector::visitNode(frontend::ASTNode const& _node)
{
if (&_node == &m_declaration)
{
if (auto const* declaration = dynamic_cast<Declaration const*>(&_node))
m_result.emplace_back(DocumentHighlight{declaration->nameLocation(), DocumentHighlightKind::Text});
else
m_result.emplace_back(DocumentHighlight{_node.location(), DocumentHighlightKind::Text});
}
return true;
}
}

View File

@ -0,0 +1,53 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/ast/ASTForward.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <libsolidity/lsp/LSPTypes.h>
namespace solidity::lsp
{
class ReferenceCollector: public frontend::ASTConstVisitor
{
public:
ReferenceCollector(frontend::Declaration const& _declaration, std::string const& _sourceIdentifierName);
static std::vector<DocumentHighlight> collect(
frontend::Declaration const* _declaration,
frontend::ASTNode const& _ast,
std::string const& _sourceIdentifierName = {}
);
void endVisit(frontend::ImportDirective const& _import) override;
void endVisit(frontend::Identifier const& _identifier) override;
void endVisit(frontend::IdentifierPath const& _identifierPath) override;
void endVisit(frontend::MemberAccess const& _memberAccess) override;
bool visitNode(frontend::ASTNode const& _node) override;
private:
bool tryAddReference(frontend::Declaration const* _declaration, solidity::langutil::SourceLocation const& _location);
private:
frontend::Declaration const& m_declaration;
std::string const& m_sourceIdentifierName;
std::vector<DocumentHighlight> m_result;
};
} // end namespace

View File

@ -0,0 +1,289 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/SemanticTokensBuilder.h>
#include <liblangutil/CharStream.h>
#include <liblangutil/SourceLocation.h>
#include <fmt/format.h>
using namespace std;
using namespace solidity::langutil;
using namespace solidity::frontend;
#if 0 // !defined(NDEBUG)
#define DPRINT(x) do { std::cerr << (x) << std::endl; } while (0)
#else
#define DPRINT(x) do {} while (false)
#endif
namespace solidity::lsp
{
namespace
{
optional<SemanticTokenType> semanticTokenTypeForType(frontend::Type const* _type)
{
if (!_type)
return nullopt;
switch (_type->category())
{
case frontend::Type::Category::Address: return SemanticTokenType::Class;
case frontend::Type::Category::Bool: return SemanticTokenType::Number;
case frontend::Type::Category::Enum: return SemanticTokenType::Enum;
case frontend::Type::Category::Function: return SemanticTokenType::Function;
case frontend::Type::Category::Integer: return SemanticTokenType::Number;
case frontend::Type::Category::RationalNumber: return SemanticTokenType::Number;
case frontend::Type::Category::StringLiteral: return SemanticTokenType::String;
case frontend::Type::Category::Struct: return SemanticTokenType::Struct;
case frontend::Type::Category::Contract: return SemanticTokenType::Class;
default:
DPRINT(fmt::format("semanticTokenTypeForType: unknown category: {}", static_cast<unsigned>(_type->category())));
return nullopt;
}
}
SemanticTokenType semanticTokenTypeForExpression(frontend::Type const* _type)
{
if (!_type)
return SemanticTokenType::Variable;
switch (_type->category())
{
case frontend::Type::Category::Enum:
return SemanticTokenType::Enum;
default:
return SemanticTokenType::Variable;
}
}
} // end namespace
Json::Value SemanticTokensBuilder::build(SourceUnit const& _sourceUnit, CharStream const& _charStream)
{
reset(&_charStream);
_sourceUnit.accept(*this);
return m_encodedTokens;
}
void SemanticTokensBuilder::reset(CharStream const* _charStream)
{
m_encodedTokens = Json::arrayValue;
m_charStream = _charStream;
m_lastLine = 0;
m_lastStartChar = 0;
}
void SemanticTokensBuilder::encode(
SourceLocation const& _sourceLocation,
SemanticTokenType _tokenType,
SemanticTokenModifiers _modifiers
)
{
/*
https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#textDocument_semanticTokens
// Step-1: Absolute positions
{ line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
{ line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 },
{ line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
// Step-2: Relative positions as intermediate step
{ deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
{ deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 },
{ deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
// Step-3: final array result
// 1st token, 2nd token, 3rd token
[ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
So traverse through the AST and assign each leaf a token 5-tuple.
*/
// solAssert(_sourceLocation.isValid());
if (!_sourceLocation.isValid())
return;
auto const [line, startChar] = m_charStream->translatePositionToLineColumn(_sourceLocation.start);
auto const length = _sourceLocation.end - _sourceLocation.start;
DPRINT(fmt::format("encode [{}:{}..{}] {}", line, startChar, length, _tokenType));
m_encodedTokens.append(line - m_lastLine);
if (line == m_lastLine)
m_encodedTokens.append(startChar - m_lastStartChar);
else
m_encodedTokens.append(startChar);
m_encodedTokens.append(length);
m_encodedTokens.append(static_cast<int>(_tokenType));
m_encodedTokens.append(static_cast<int>(_modifiers));
m_lastLine = line;
m_lastStartChar = startChar;
}
bool SemanticTokensBuilder::visit(frontend::ContractDefinition const& _node)
{
encode(_node.nameLocation(), SemanticTokenType::Class);
return true;
}
bool SemanticTokensBuilder::visit(frontend::ElementaryTypeName const& _node)
{
encode(_node.location(), SemanticTokenType::Type);
return true;
}
bool SemanticTokensBuilder::visit(frontend::ElementaryTypeNameExpression const& _node)
{
if (auto const tokenType = semanticTokenTypeForType(_node.annotation().type); tokenType.has_value())
encode(_node.location(), tokenType.value());
return true;
}
bool SemanticTokensBuilder::visit(frontend::EnumDefinition const& _node)
{
encode(_node.nameLocation(), SemanticTokenType::Enum);
return true;
}
bool SemanticTokensBuilder::visit(frontend::EnumValue const& _node)
{
encode(_node.nameLocation(), SemanticTokenType::EnumMember);
return true;
}
bool SemanticTokensBuilder::visit(frontend::ErrorDefinition const& _node)
{
encode(_node.nameLocation(), SemanticTokenType::Event);
return true;
}
bool SemanticTokensBuilder::visit(frontend::FunctionDefinition const& _node)
{
encode(_node.nameLocation(), SemanticTokenType::Function);
return true;
}
void SemanticTokensBuilder::endVisit(frontend::Literal const& _literal)
{
encode(_literal.location(), SemanticTokenType::Number);
}
void SemanticTokensBuilder::endVisit(frontend::Identifier const& _identifier)
{
DPRINT(fmt::format("Identifier: {}, {}..{} cat={}", _identifier.name(), _identifier.location().start, _identifier.location().end, _identifier.annotation().type->category()));
SemanticTokenModifiers modifiers = SemanticTokenModifiers::None;
if (_identifier.annotation().isConstant.set() && *_identifier.annotation().isConstant)
{
DPRINT("OMG We've found a const!");
modifiers = modifiers | SemanticTokenModifiers::Readonly;
}
encode(_identifier.location(), semanticTokenTypeForExpression(_identifier.annotation().type), modifiers);
}
void SemanticTokensBuilder::endVisit(frontend::IdentifierPath const& _node)
{
DPRINT(fmt::format("IdentifierPath: identifier path [{}..{}]", _node.location().start, _node.location().end));
for (size_t i = 0; i < _node.path().size(); ++i)
DPRINT(fmt::format(" [{}]: {}", i, _node.path().at(i)));
if (dynamic_cast<EnumDefinition const*>(_node.annotation().referencedDeclaration))
encode(_node.location(), SemanticTokenType::EnumMember);
else
encode(_node.location(), SemanticTokenType::Variable);
}
bool SemanticTokensBuilder::visit(frontend::MemberAccess const& _node)
{
DPRINT(fmt::format("[{}..{}] MemberAccess({}): {}", _node.location().start, _node.location().end, _node.annotation().referencedDeclaration ? _node.annotation().referencedDeclaration->name() : "?", _node.memberName()));
auto const memberNameLength = static_cast<int>(_node.memberName().size());
auto const memberTokenType = semanticTokenTypeForExpression(_node.annotation().type);
auto lhsLocation = _node.location();
lhsLocation.end -= (memberNameLength + 1 /*exclude the dot*/);
auto rhsLocation = _node.location();
rhsLocation.start = rhsLocation.end - static_cast<int>(memberNameLength);
if (memberTokenType == SemanticTokenType::Enum)
{
// Special handling for enumeration symbols.
encode(lhsLocation, SemanticTokenType::Enum);
encode(rhsLocation, SemanticTokenType::EnumMember);
}
else if (memberTokenType == SemanticTokenType::Function)
{
// Special handling for function symbols.
encode(lhsLocation, SemanticTokenType::Variable);
encode(rhsLocation, memberTokenType);
}
else
{
encode(rhsLocation, memberTokenType);
}
return false; // we handle LHS and RHS explicitly above.
}
bool SemanticTokensBuilder::visit(frontend::ParameterList const& _node)
{
(void) _node;
for (ASTPointer<VariableDeclaration> const& parameter: _node.parameters())
{
// NOTE: Should only highlight the name but it seems that nameLocation()
// also contains the type name.
encode(parameter->nameLocation(), SemanticTokenType::Parameter);
}
return false; // do not descent into child nodes
}
void SemanticTokensBuilder::endVisit(PragmaDirective const& _pragma)
{
encode(_pragma.location(), SemanticTokenType::Macro);
// NOTE: It would be nice if we could highlight based on the symbols,
// such as (version) numerics be different than identifiers.
}
bool SemanticTokensBuilder::visit(frontend::UserDefinedTypeName const& _node)
{
if (auto const token = semanticTokenTypeForType(_node.annotation().type); token.has_value())
encode(_node.location(), *token);
return false;
}
bool SemanticTokensBuilder::visit(frontend::VariableDeclaration const& _node)
{
DPRINT(fmt::format("VariableDeclaration: {}", _node.name()));
if (auto const token = semanticTokenTypeForType(_node.typeName().annotation().type); token.has_value())
encode(_node.typeName().location(), *token);
encode(_node.nameLocation(), SemanticTokenType::Variable);
if (_node.overrides())
_node.overrides()->accept(*this);
if (_node.value())
_node.value()->accept(*this);
return false;
}
} // end namespace

View File

@ -0,0 +1,122 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <json/json.h>
#include <fmt/format.h>
namespace solidity::langutil
{
class CharStream;
struct SourceLocation;
}
namespace solidity::lsp
{
// See: https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#semanticTokenTypes
enum class SemanticTokenType
{
Class,
Comment,
Enum,
EnumMember,
Event,
Function,
Interface,
Keyword,
Macro,
Method,
Modifier,
Number,
Operator,
Parameter,
Property,
String,
Struct,
Type,
TypeParameter,
Variable,
// Unused below:
// Namespace,
// Regexp,
};
enum class SemanticTokenModifiers
{
None = 0,
// Member integer values must be bit-values as
// they can be OR'd together.
Abstract = 0x0001,
Declaration = 0x0002,
Definition = 0x0004,
Deprecated = 0x0008,
Documentation = 0x0010,
Modification = 0x0020,
Readonly = 0x0040,
// Unused below:
// Static,
// Async,
// DefaultLibrary,
};
constexpr SemanticTokenModifiers operator|(SemanticTokenModifiers a, SemanticTokenModifiers b) noexcept
{
return static_cast<SemanticTokenModifiers>(static_cast<int>(a) | static_cast<int>(b));
}
class SemanticTokensBuilder: public frontend::ASTConstVisitor
{
public:
Json::Value build(frontend::SourceUnit const& _sourceUnit, langutil::CharStream const& _charStream);
void reset(langutil::CharStream const* _charStream);
void encode(
langutil::SourceLocation const& _sourceLocation,
SemanticTokenType _tokenType,
SemanticTokenModifiers _modifiers = SemanticTokenModifiers::None
);
bool visit(frontend::ContractDefinition const&) override;
bool visit(frontend::ElementaryTypeName const&) override;
bool visit(frontend::ElementaryTypeNameExpression const&) override;
bool visit(frontend::EnumDefinition const&) override;
bool visit(frontend::EnumValue const&) override;
bool visit(frontend::ErrorDefinition const&) override;
bool visit(frontend::FunctionDefinition const&) override;
void endVisit(frontend::Literal const&) override;
void endVisit(frontend::Identifier const&) override;
void endVisit(frontend::IdentifierPath const&) override;
bool visit(frontend::MemberAccess const&) override;
bool visit(frontend::ParameterList const&) override;
void endVisit(frontend::PragmaDirective const&) override;
bool visit(frontend::UserDefinedTypeName const&) override;
bool visit(frontend::VariableDeclaration const&) override;
private:
Json::Value m_encodedTokens;
langutil::CharStream const* m_charStream;
int m_lastLine;
int m_lastStartChar;
};
} // end namespace

View File

@ -0,0 +1,138 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/Transport.h>
#include <libsolutil/JSON.h>
#include <libsolutil/Visitor.h>
#include <boost/algorithm/string.hpp>
#include <iostream>
#include <sstream>
using namespace std;
using namespace solidity::lsp;
JSONTransport::JSONTransport(istream& _in, ostream& _out):
m_input{_in},
m_output{_out}
{
}
JSONTransport::JSONTransport(): JSONTransport(cin, cout)
{
}
bool JSONTransport::closed() const noexcept
{
return m_input.eof();
}
optional<Json::Value> JSONTransport::receive()
{
auto const headers = parseHeaders();
if (!headers)
return nullopt;
if (!headers->count("content-length"))
return nullopt;
string const data = readBytes(stoi(headers->at("content-length")));
Json::Value jsonMessage;
string errs;
solidity::util::jsonParseStrict(data, jsonMessage, &errs);
if (!errs.empty())
return nullopt; // JsonParseError
//traceMessage(jsonMessage, "Request");
return {jsonMessage};
}
void JSONTransport::notify(string const& _method, Json::Value const& _message)
{
Json::Value json;
json["method"] = _method;
json["params"] = _message;
send(json);
}
void JSONTransport::reply(MessageID _id, Json::Value const& _message)
{
Json::Value json;
json["result"] = _message;
send(json, _id);
}
void JSONTransport::error(MessageID _id, ErrorCode _code, string const& _message)
{
Json::Value json;
json["error"]["code"] = static_cast<int>(_code);
json["error"]["message"] = _message;
send(json, _id);
}
void JSONTransport::send(Json::Value _json, MessageID _id)
{
_json["jsonrpc"] = "2.0";
if (!_id.isNull())
_json["id"] = _id;
string const jsonString = solidity::util::jsonCompactPrint(_json);
m_output << "Content-Length: " << jsonString.size() << "\r\n";
m_output << "\r\n";
m_output << jsonString;
m_output.flush();
//traceMessage(_json, "Response");
}
optional<map<string, string>> JSONTransport::parseHeaders()
{
map<string, string> headers;
while (true)
{
string line;
getline(m_input, line);
if (boost::trim_copy(line).empty())
break;
auto const delimiterPos = line.find(':');
if (delimiterPos == string::npos)
return nullopt;
auto const name = boost::to_lower_copy(line.substr(0, delimiterPos));
auto const value = boost::trim_copy(line.substr(delimiterPos + 1));
headers[move(name)] = value;
}
return {move(headers)};
}
string JSONTransport::readBytes(int _n)
{
if (_n < 0)
return {};
string data;
data.resize(static_cast<string::size_type>(_n));
m_input.read(data.data(), _n);
return data;
}

105
libsolidity/lsp/Transport.h Normal file
View File

@ -0,0 +1,105 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <json/value.h>
#include <functional>
#include <iosfwd>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
namespace solidity::lsp
{
using MessageID = Json::Value;
enum class ErrorCode
{
// Defined by JSON RPC
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
serverErrorStart = -32099,
serverErrorEnd = -32000,
ServerNotInitialized = -32002,
UnknownErrorCode = -32001,
// Defined by the protocol.
RequestCancelled = -32800,
ContentModified = -32801,
};
/// Transport layer API
///
/// The transport layer API is abstracted so it users become more testable as well as
/// this way it could be possible to support other transports (HTTP for example) easily.
class Transport
{
public:
virtual ~Transport() = default;
virtual bool closed() const noexcept = 0;
virtual std::optional<Json::Value> receive() = 0;
virtual void notify(std::string const& _method, Json::Value const& _params) = 0;
virtual void reply(MessageID _id, Json::Value const& _result) = 0;
virtual void error(MessageID _id, ErrorCode _code, std::string const& _message) = 0;
};
/// LSP Transport using JSON-RPC over iostreams.
class JSONTransport : public Transport
{
public:
/// Constructs a standard stream transport layer.
///
/// @param _in for example std::cin (stdin)
/// @param _out for example std::cout (stdout)
JSONTransport(std::istream& _in, std::ostream& _out);
// Constructs a JSON transport using standard I/O streams.
JSONTransport();
bool closed() const noexcept override;
std::optional<Json::Value> receive() override;
void notify(std::string const& _method, Json::Value const& _params) override;
void reply(MessageID _id, Json::Value const& _result) override;
void error(MessageID _id, ErrorCode _code, std::string const& _message) override;
protected:
/// Reads given number of bytes from the client.
virtual std::string readBytes(int _n);
/// Sends an arbitrary raw message to the client.
///
/// Used by the notify/reply/error function family.
virtual void send(Json::Value _message, MessageID _id = Json::nullValue);
/// Parses header section from the client including message-delimiting empty line.
std::optional<std::map<std::string, std::string>> parseHeaders();
private:
std::istream& m_input;
std::ostream& m_output;
};
} // end namespace

410
scripts/test_solidity_lsp.py Executable file
View File

@ -0,0 +1,410 @@
#!/usr/bin/env python3.9
# DEPENDENCIES:
# pip install git+https://github.com/christianparpart/pylspclient.git --user
# WHEN DEVELOPING (editable module):
# pip install -e /path/to/checkout/pylspclient
import argparse
import os
import subprocess
import threading
from pprint import pprint
from threading import Condition
from deepdiff import DeepDiff
# Requires the one from https://github.com/christianparpart/pylspclient
# Use `pip install -e $PATH_TO_LIB_CHECKOUT --user` for local development & testing
import pylspclient
lsp_types = pylspclient.lsp_structs
SGR_RESET = '\033[m'
SGR_TEST_BEGIN = '\033[1;33m'
SGR_STATUS_OKAY = '\033[1;32m'
SGR_STATUS_FAIL = '\033[1;31m'
SGR_INSPECT = '\033[1;35m'
TEST_NAME = 'test_definition'
def dprint(text: str):
print(SGR_INSPECT + "-- " + text + ":" + SGR_RESET)
def dinspect(text, obj):
dprint(text)
if not obj is None:
pprint(obj)
class ReadPipe(threading.Thread):
"""
Used to link (solc) process stdio.
"""
def __init__(self, pipe):
threading.Thread.__init__(self)
self.pipe = pipe
def run(self):
dprint("ReadPipe: starting")
line = self.pipe.readline().decode('utf-8')
while line:
print(line)
#print("\033[1;42m{}\033[m\n".format(line))
line = self.pipe.readline().decode('utf-8')
SOLIDITY_LANGUAGE_ID = "solidity" # lsp_types.LANGUAGE_IDENTIFIER.C
LSP_CLIENT_CAPS = {
'textDocument': {'codeAction': {'dynamicRegistration': True},
'codeLens': {'dynamicRegistration': True},
'colorProvider': {'dynamicRegistration': True},
'completion': {'completionItem': {'commitCharactersSupport': True,
'documentationFormat': ['markdown', 'plaintext'],
'snippetSupport': True},
'completionItemKind': {'valueSet': [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25
]},
'contextSupport': True,
'dynamicRegistration': True},
'definition': {'dynamicRegistration': True},
'documentHighlight': {'dynamicRegistration': True},
'documentLink': {'dynamicRegistration': True},
'documentSymbol': {
'dynamicRegistration': True,
'symbolKind': {'valueSet': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26
]}
},
'formatting': {'dynamicRegistration': True},
'hover': {'contentFormat': ['markdown', 'plaintext'],
'dynamicRegistration': True},
'implementation': {'dynamicRegistration': True},
'onTypeFormatting': {'dynamicRegistration': True},
'publishDiagnostics': {'relatedInformation': True},
'rangeFormatting': {'dynamicRegistration': True},
'references': {'dynamicRegistration': True},
'rename': {'dynamicRegistration': True},
'signatureHelp': {'dynamicRegistration': True,
'signatureInformation': {'documentationFormat': ['markdown', 'plaintext']}},
'synchronization': {'didSave': True,
'dynamicRegistration': True,
'willSave': True,
'willSaveWaitUntil': True},
'typeDefinition': {'dynamicRegistration': True}},
'workspace': {'applyEdit': True,
'configuration': True,
'didChangeConfiguration': {'dynamicRegistration': True},
'didChangeWatchedFiles': {'dynamicRegistration': True},
'executeCommand': {'dynamicRegistration': True},
'symbol': {
'dynamicRegistration': True,
'symbolKind': {'valueSet': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26 ]}
},
'workspaceEdit': {'documentChanges': True},
'workspaceFolders': True}
}
class SolcInstance:
"""
Manages the solc executable instance and provides the handle to communicate with it
"""
process: subprocess.Popen
endpoint: pylspclient.LspEndpoint
client: pylspclient.LspClient
read_pipe: ReadPipe
diagnostics_cond: Condition
#published_diagnostics: object
def __init__(self, _solc_path: str) -> None:
self.solc_path = _solc_path
self.published_diagnostics = []
self.client = pylspclient.LspClient(None)
self.diagnostics_cond = Condition()
def __enter__(self):
dprint(f"Starting solc LSP instance: {self.solc_path}")
self.process = subprocess.Popen(
[self.solc_path, "--lsp"],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
self.read_pipe = ReadPipe(self.process.stderr)
self.read_pipe.start()
self.endpoint = pylspclient.LspEndpoint(
json_rpc_endpoint=pylspclient.JsonRpcEndpoint(
self.process.stdin,
self.process.stdout
),
notify_callbacks={
'textDocument/publishDiagnostics': self.on_publish_diagnostics
}
)
self.client = pylspclient.LspClient(self.endpoint)
return self
def __exit__(self, _exception_type, _exception_value, _traceback) -> None:
dprint("Stopping solc instance.")
self.client.shutdown()
self.client.exit()
self.read_pipe.join()
def on_publish_diagnostics(self, _diagnostics) -> None:
dprint("Receiving published diagnostics:")
pprint(_diagnostics)
self.published_diagnostics.append(_diagnostics)
self.diagnostics_cond.acquire()
self.diagnostics_cond.notify()
self.diagnostics_cond.release()
class SolcTests:
def __init__(self, _client: SolcInstance, _project_root_dir: str):
self.solc = _client
self.project_root_dir = _project_root_dir
self.project_root_uri = 'file://' + self.project_root_dir
self.tests = 0
dprint("root dir: {self.project_root_dir}")
# {{{ helpers
def get_test_file_path(self, _test_case_name):
return f"{self.project_root_dir}/{_test_case_name}.sol"
def get_test_file_uri(self, _test_case_name):
return "file://" + self.get_test_file_path(_test_case_name)
def get_test_file_contents(self, _test_case_name):
return open(self.get_test_file_path(_test_case_name,), mode="r", encoding="utf-8").read()
def lsp_open_file(self, _test_case_name):
""" Opens file for given test case. """
version = 1
file_uri = self.get_test_file_uri(_test_case_name)
file_contents = self.get_test_file_contents(_test_case_name)
self.solc.client.didOpen(lsp_types.TextDocumentItem(
file_uri, SOLIDITY_LANGUAGE_ID, version, file_contents
))
def lsp_open_file_and_wait_for_diagnostics(self, _test_case_name):
"""
Opens file for given test case and waits for diagnostics to be published.
"""
self.solc.diagnostics_cond.acquire()
self.lsp_open_file(_test_case_name)
self.solc.diagnostics_cond.wait_for(
predicate=lambda: len(self.solc.published_diagnostics) != 0,
timeout=2.0
)
self.solc.diagnostics_cond.release()
def expect(self, _cond: bool, _description: str) -> None:
self.tests = self.tests + 1
prefix = f"[{self.tests}] {SGR_TEST_BEGIN}{_description}{SGR_RESET} : "
if _cond:
print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET)
else:
print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET)
raise RuntimeError("Expectation failed.")
def expect_equal(self, _description: str, _actual, _expected) -> None:
self.tests = self.tests + 1
prefix = f"[{self.tests}] {SGR_TEST_BEGIN}{_description}: "
diff = DeepDiff(_actual, _expected)
if len(diff) == 0:
print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET)
return
print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET)
pprint(diff)
raise RuntimeError('Expectation failed.')
# }}}
# {{{ actual tests
def run(self):
self.open_files_and_test_publish_diagnostics()
self.test_definition()
self.test_documentHighlight()
# self.test_hover()
# self.test_implementation()
# self.test_references()
# self.test_signatureHelp()
# self.test_semanticTokensFull()
def extract_test_file_name(self, _uri: str):
"""
Extracts the project-root URI prefix from the URI.
"""
subLength = len(self.project_root_uri)
return _uri[subLength:]
def open_files_and_test_publish_diagnostics(self):
self.lsp_open_file_and_wait_for_diagnostics(TEST_NAME)
# should have received one published_diagnostics notification
dprint("len: {len(self.solc.published_diagnostics)}")
self.expect(len(self.solc.published_diagnostics) == 1, "one published_diagnostics message")
published_diagnostics = self.solc.published_diagnostics[0]
self.expect(published_diagnostics['uri'] == self.get_test_file_uri(TEST_NAME),
'diagnostic: uri')
# containing one single diagnostics report
diagnostics = published_diagnostics['diagnostics']
self.expect(len(diagnostics) == 1, "one diagnostics")
diagnostic = diagnostics[0]
self.expect(diagnostic['code'] == 3805, 'diagnostic: pre-release compiler')
self.expect_equal(
'check range',
diagnostic['range'],
{'end': {'character': 0, 'line': 0}, 'start': {'character': 0, 'line': 0}}
)
def test_definition(self):
"""
Tests goto-definition. The following tokens can be used to jump from:
"""
self.solc.published_diagnostics.clear()
# LHS enum variable in assignment: `weather`
result = self.solc.client.definition(
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
lsp_types.Position(23, 9)) # line/col numbers are 0-based
self.expect(len(result) == 1, "only one definition returned")
self.expect(result[0].range == lsp_types.Range(lsp_types.Position(19, 16),
lsp_types.Position(19, 23)), "range check")
# Test on return parameter symbol: `result` at 35:9 (begin of identifier)
result = self.solc.client.definition(
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
lsp_types.Position(24, 8))
# Test goto-def of a function-parameter.
result = self.solc.client.definition(
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
lsp_types.Position(24, 17))
# Test on function parameter symbol
# Test on enum type symbol in expression
# Test on enum value symbol in expression
# Test on import statement to jump to imported file
def test_documentHighlight(self):
"""
Tests symbolic hover-hints, that is, highlighting all other symbols
that are semantically the same.
- hovering a variable symbol will highlight all occurrences of that variable.
- hovering a function symbol will hover all other calls to it as well
as the function definition's function name itself.
- hovering an enum value will highlight all other uses of that enum value,
including the name of the definition of that enum value.
- hovering an enum type will highlight all other uses of that enum type,
including the name of the definition of that enum type.
- anything else will reply with an empty set.
"""
# variable
reply = self.solc.client.documentHighlight(
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
lsp_types.Position(23, 9) # line/col numbers are 0-based
)
self.expect(len(reply) == 2, "2 highlights")
self.expect(reply[0].kind == lsp_types.DocumentHighlightKind.Text, "kind")
self.expect(reply[0].range == lsp_types.Range(lsp_types.Position(19, 16),
lsp_types.Position(19, 23)), "range check")
self.expect(reply[1].kind == lsp_types.DocumentHighlightKind.Text, "kind")
self.expect(reply[1].range == lsp_types.Range(lsp_types.Position(23, 8),
lsp_types.Position(23, 15)), "range check")
# enum type: Weather, line 24, col 19 .. 25
reply = self.solc.client.documentHighlight(
lsp_types.TextDocumentIdentifier(self.get_test_file_uri(TEST_NAME)),
lsp_types.Position(23, 18) # line/col numbers are 0-based
)
ENUM_TYPE_HIGHLIGHTS = [
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 6, 'from': 5, 'to': 12 },
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 14, 'from': 4, 'to': 11 },
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 14, 'from': 27, 'to': 34 },
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 19, 'from': 8, 'to': 15 },
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 19, 'from': 26, 'to': 33 },
{ 'kind': lsp_types.DocumentHighlightKind.Text, 'line': 23, 'from': 18, 'to': 25 }
]
self.expect(len(reply) == len(ENUM_TYPE_HIGHLIGHTS), f"expect {len(ENUM_TYPE_HIGHLIGHTS)} highlights")
for i in range(0, len(reply)):
self.expect(reply[i].kind == ENUM_TYPE_HIGHLIGHTS[i]['kind'], "check kind")
self.expect(
reply[i].range == lsp_types.Range(
lsp_types.Position(
ENUM_TYPE_HIGHLIGHTS[i]['line'],
ENUM_TYPE_HIGHLIGHTS[i]['from']
),
lsp_types.Position(
ENUM_TYPE_HIGHLIGHTS[i]['line'],
ENUM_TYPE_HIGHLIGHTS[i]['to']
)
),
"range check"
)
def test_references(self):
# Shows all references of given symbol def
pass
def test_hover(self):
pass
# }}}
class SolidityLSPTestSuite: # {{{
def __init__(self):
self.project_root_dir = ''
self.solc_path = ''
def main(self):
solc_path, project_root_dir = self.parse_args_and_prepare()
with SolcInstance(solc_path) as solc:
project_root_uri = 'file://' + project_root_dir
workspace_folders = [ {'name': 'solidity-lsp', 'uri': project_root_uri} ]
traceServer = 'off'
solc.client.initialize(solc.process.pid, None, project_root_uri,
None, LSP_CLIENT_CAPS, traceServer,
workspace_folders)
solc.client.initialized()
tests = SolcTests(solc, project_root_dir)
tests.run()
def parse_args_and_prepare(self):
"""
Parses CLI args and retruns tuple of path to solc executable
and path to solidity-project root dir.
"""
parser = argparse.ArgumentParser(description='Solidity LSP Test suite')
parser.add_argument(
'solc_path',
type=str,
default="/home/trapni/work/solidity/build/solc/solc",
help='Path to solc binary to test against',
nargs="?"
)
parser.add_argument(
'project_root_dir',
type=str,
default=f"{os.path.dirname(os.path.realpath(__file__))}/..",
help='Path to Solidity project\'s root directory (must be fully qualified).',
nargs="?"
)
args = parser.parse_args()
project_root_dir = os.path.realpath(args.project_root_dir) + '/test/libsolidity/lsp'
return [args.solc_path, project_root_dir]
# }}}
if __name__ == "__main__":
suite = SolidityLSPTestSuite()
suite.main()

View File

@ -3,8 +3,24 @@ set(libsolcli_sources
CommandLineParser.cpp CommandLineParser.h
)
set(libsolcli_libs solidity Boost::boost Boost::program_options)
if(${CMAKE_BUILD_TYPE} STREQUAL "Debug")
# Since we do not want to depend on networking code on production binaries,
# this feature is only available when creating a debug build.
# the LSP's TCP listener is exclusively used more convenient debugging.
option(SOLC_LSP_TCP "Solidity LSP: Enables TCP listener support (should only be eanbled for debugging purposes)." OFF)
if(SOLC_LSP_TCP)
set(libsolcli_defines SOLC_LSP_TCP=1)
list(APPEND libsolcli_sources LSPTCPTransport.cpp LSPTCPTransport.h)
find_package(Threads REQUIRED)
list(APPEND libsolcli_libs Threads::Threads)
endif()
endif()
add_library(solcli ${libsolcli_sources})
target_link_libraries(solcli PUBLIC solidity Boost::boost Boost::program_options)
target_compile_definitions(solcli PUBLIC ${libsolcli_defines})
target_link_libraries(solcli PUBLIC ${libsolcli_libs})
set(sources main.cpp)

View File

@ -24,6 +24,11 @@
#include <solc/CommandLineInterface.h>
#include "license.h"
#if defined(SOLC_LSP_TCP)
#include <solc/LSPTCPTransport.h>
#endif
#include "solidity/BuildInfo.h"
#include <libsolidity/interface/Version.h>
@ -36,6 +41,8 @@
#include <libsolidity/interface/DebugSettings.h>
#include <libsolidity/interface/ImportRemapper.h>
#include <libsolidity/interface/StorageLayout.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/Transport.h>
#include <libyul/AssemblyStack.h>
@ -53,7 +60,10 @@
#include <libsolutil/CommonIO.h>
#include <libsolutil/JSON.h>
#include <range/v3/all.hpp>
#include <algorithm>
#include <fstream>
#include <memory>
#include <range/v3/view/map.hpp>
@ -507,7 +517,11 @@ bool CommandLineInterface::readInputFiles()
m_fileReader.setStdin(readUntilEnd(m_sin));
}
if (m_fileReader.sourceCodes().empty() && !m_standardJsonInput.has_value())
if (
m_options.input.mode != InputMode::LanguageServer &&
m_fileReader.sourceCodes().empty() &&
!m_standardJsonInput.has_value()
)
{
serr() << "All specified input files either do not exist or are not regular files." << endl;
return false;
@ -623,6 +637,9 @@ bool CommandLineInterface::processInput()
m_standardJsonInput.reset();
break;
}
case InputMode::LanguageServer:
serveLSP();
break;
case InputMode::Assembler:
if (!assemble(m_options.assembly.inputLanguage, m_options.assembly.targetMachine))
return false;
@ -887,6 +904,21 @@ void CommandLineInterface::handleAst()
}
}
void CommandLineInterface::serveLSP()
{
std::unique_ptr<lsp::Transport> transport = make_unique<lsp::JSONTransport>();
#if defined(SOLC_LSP_TCP)
if (m_options.lsp.port.has_value())
{
unsigned const port = m_options.lsp.port.value();
transport = make_unique<lsp::LSPTCPTransport>(static_cast<unsigned short>(port), traceLevel, traceLogger);
}
#endif
lsp::LanguageServer languageServer(move(transport));
languageServer.run();
}
bool CommandLineInterface::link()
{
solAssert(m_options.input.mode == InputMode::Linker, "");

View File

@ -66,6 +66,7 @@ private:
void printVersion();
void printLicense();
bool compile();
void serveLSP();
bool link();
void writeLinkedFiles();
/// @returns the ``// <identifier> -> name`` hint for library placeholders.

View File

@ -63,6 +63,7 @@ static string const g_strIPFS = "ipfs";
static string const g_strLicense = "license";
static string const g_strLibraries = "libraries";
static string const g_strLink = "link";
static string const g_strLSP = "lsp";
static string const g_strMachine = "machine";
static string const g_strMetadataHash = "metadata-hash";
static string const g_strMetadataLiteral = "metadata-literal";
@ -136,6 +137,7 @@ static map<InputMode, string> const g_inputModeName = {
{InputMode::Assembler, "assembler"},
{InputMode::StandardJson, "standard JSON"},
{InputMode::Linker, "linker"},
{InputMode::LanguageServer, "language server (LSP)"},
};
bool CommandLineParser::checkMutuallyExclusive(vector<string> const& _optionNames)
@ -243,6 +245,10 @@ bool CommandLineOptions::operator==(CommandLineOptions const& _other) const noex
optimizer.expectedExecutionsPerDeployment == _other.optimizer.expectedExecutionsPerDeployment &&
optimizer.noOptimizeYul == _other.optimizer.noOptimizeYul &&
optimizer.yulSteps == _other.optimizer.yulSteps &&
lsp.trace == _other.lsp.trace &&
#if defined(SOLC_LSP_TCP)
lsp.port == _other.lsp.port &&
#endif
modelChecker.initialize == _other.modelChecker.initialize &&
modelChecker.settings == _other.modelChecker.settings;
}
@ -334,7 +340,7 @@ bool CommandLineParser::parseInputPathsAndRemappings()
// Keep it working that way for backwards-compatibility.
m_options.input.addStdin = true;
}
else if (m_options.input.paths.size() == 0 && !m_options.input.addStdin)
else if (m_options.input.paths.size() == 0 && !m_options.input.addStdin && m_options.input.mode != InputMode::LanguageServer)
{
serr() << "No input files given. If you wish to use the standard input please specify \"-\" explicitly." << endl;
return false;
@ -472,6 +478,7 @@ bool CommandLineParser::parseOutputSelection()
return contains(assemblerModeOutputs, _outputName);
case InputMode::StandardJson:
case InputMode::Linker:
case InputMode::LanguageServer:
return false;
}
@ -644,6 +651,11 @@ General Information)").c_str(),
"Supported Inputs is the output of the --" + g_strStandardJSON + " or the one produced by "
"--" + g_strCombinedJson + " " + CombinedJsonRequests::componentName(&CombinedJsonRequests::ast)).c_str()
)
(
g_strLSP.c_str(),
"Switch to language server mode (\"LSP\"). Allows the compiler to be used as an analysis backend "
"for your favourite IDE."
)
;
desc.add(alternativeInputModes);
@ -779,6 +791,22 @@ General Information)").c_str(),
;
desc.add(optimizerOptions);
po::options_description lspOptions("LSP Options");
lspOptions.add_options()
#if defined(SOLC_LSP_TCP)
(
"lsp-port",
po::value<unsigned>()->value_name("PORT"),
"Enables LSP over TCP instead of stdio and uses the specified TCP port."
)
#endif
(
"lsp-trace",
po::value<string>()->value_name("FILE"),
"Enables LSP request tracing to be logged into the specified file."
);
desc.add(lspOptions);
po::options_description smtCheckerOptions("Model Checker Options");
smtCheckerOptions.add_options()
(
@ -879,6 +907,7 @@ bool CommandLineParser::processArgs()
g_strStrictAssembly,
g_strYul,
g_strImportAst,
g_strLSP
}))
return false;
@ -890,6 +919,8 @@ bool CommandLineParser::processArgs()
m_options.input.mode = InputMode::Version;
else if (m_args.count(g_strStandardJSON) > 0)
m_options.input.mode = InputMode::StandardJson;
else if (m_args.count(g_strLSP))
m_options.input.mode = InputMode::LanguageServer;
else if (m_args.count(g_strAssemble) > 0 || m_args.count(g_strStrictAssembly) > 0 || m_args.count(g_strYul) > 0)
m_options.input.mode = InputMode::Assembler;
else if (m_args.count(g_strLink) > 0)
@ -946,7 +977,8 @@ bool CommandLineParser::processArgs()
if (
m_options.input.mode != InputMode::Compiler &&
m_options.input.mode != InputMode::CompilerWithASTImport &&
m_options.input.mode != InputMode::Assembler
m_options.input.mode != InputMode::Assembler &&
m_options.input.mode != InputMode::LanguageServer
)
{
if (!m_args[g_strOptimizeRuns].defaulted())
@ -1235,6 +1267,22 @@ bool CommandLineParser::processArgs()
}
}
if (m_args.count("lsp-trace"))
m_options.lsp.trace = boost::filesystem::path(m_args.at("lsp-trace").as<string>());
#if defined(SOLC_LSP_TCP)
if (m_args.count("lsp-port"))
{
unsigned const port = m_args.at("lsp-port").as<unsigned>();
if (port > 0xFFFF)
{
serr() << "LSP port number not in valid port range " << port << '.' << endl;
return false;
}
m_options.lsp.port = port;
}
#endif
if (m_args.count(g_strModelCheckerContracts))
{
string contractsStr = m_args[g_strModelCheckerContracts].as<string>();
@ -1318,7 +1366,11 @@ bool CommandLineParser::processArgs()
if (m_options.input.mode == InputMode::Compiler)
m_options.input.errorRecovery = (m_args.count(g_strErrorRecovery) > 0);
solAssert(m_options.input.mode == InputMode::Compiler || m_options.input.mode == InputMode::CompilerWithASTImport);
solAssert(
m_options.input.mode == InputMode::Compiler ||
m_options.input.mode == InputMode::CompilerWithASTImport ||
m_options.input.mode == InputMode::LanguageServer
);
return true;
}

View File

@ -56,6 +56,7 @@ enum class InputMode
StandardJson,
Linker,
Assembler,
LanguageServer
};
struct CompilerOutputs
@ -226,6 +227,14 @@ struct CommandLineOptions
std::optional<std::string> yulSteps;
} optimizer;
struct
{
boost::filesystem::path trace;
#if defined(SOLC_LSP_TCP)
std::optional<unsigned> port;
#endif
} lsp;
struct
{
bool initialize = false;

115
solc/LSPTCPTransport.cpp Normal file
View File

@ -0,0 +1,115 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <solc/LSPTCPTransport.h>
#include <libsolutil/JSON.h>
#include <fmt/format.h>
#include <optional>
#include <string>
#include <iostream>
namespace solidity::lsp
{
using std::function;
using std::nullopt;
using std::optional;
using std::string_view;
using std::to_string;
using namespace std::string_literals;
LSPTCPTransport::LSPTCPTransport(unsigned short _port, Trace _traceLevel, function<void(string_view)> _trace):
m_io_service(),
m_endpoint(boost::asio::ip::make_address("127.0.0.1"), _port),
m_acceptor(m_io_service),
m_stream(),
m_jsonTransport(),
m_traceLevel{_traceLevel},
m_trace(_trace ? std::move(_trace) : [](string_view) {})
{
m_acceptor.open(m_endpoint.protocol());
m_acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
m_acceptor.bind(m_endpoint);
m_acceptor.listen();
if (m_traceLevel >= Trace::Messages)
m_trace("Listening on tcp://127.0.0.1:"s + to_string(_port));
}
void LSPTCPTransport::setTraceLevel(Trace _traceLevel)
{
m_traceLevel = _traceLevel;
if (m_jsonTransport)
m_jsonTransport->setTraceLevel(_traceLevel);
}
bool LSPTCPTransport::closed() const noexcept
{
return !m_acceptor.is_open();
}
optional<Json::Value> LSPTCPTransport::receive()
{
auto const clientClosed = [&]() { return !m_stream || !m_stream.value().good() || m_stream.value().eof(); };
if (clientClosed())
{
m_stream.emplace(m_acceptor.accept());
if (clientClosed())
return nullopt;
auto const remoteAddr = m_stream.value().socket().remote_endpoint().address().to_string();
auto const remotePort = m_stream.value().socket().remote_endpoint().port();
m_trace("New client connected from "s + remoteAddr + ":" + to_string(remotePort) + ".");
m_jsonTransport.emplace(m_stream.value(), m_stream.value(), m_traceLevel, [this](auto msg) { m_trace(msg); });
}
if (auto value = m_jsonTransport.value().receive(); value.has_value())
return value;
if (clientClosed())
{
m_trace("Client disconnected.");
m_jsonTransport.reset();
m_stream.reset();
}
return nullopt;
}
void LSPTCPTransport::notify(std::string const& _method, Json::Value const& _params)
{
if (m_jsonTransport.has_value())
m_jsonTransport.value().notify(_method, _params);
}
void LSPTCPTransport::reply(MessageID _id, Json::Value const& _result)
{
fmt::print("reply: {}\n{}\n", util::jsonPrettyPrint(_id), util::jsonPrettyPrint(_result));
if (m_jsonTransport.has_value())
m_jsonTransport.value().reply(_id, _result);
}
void LSPTCPTransport::error(MessageID _id, ErrorCode _code, std::string const& _message)
{
if (m_jsonTransport.has_value())
m_jsonTransport.value().error(_id, _code, _message);
}
}

49
solc/LSPTCPTransport.h Normal file
View File

@ -0,0 +1,49 @@
/*
This file is part of solidity.
solidity is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
solidity is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with solidity. If not, see <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/lsp/Transport.h>
#include <boost/asio.hpp>
#include <optional>
namespace solidity::lsp
{
class LSPTCPTransport: public lsp::Transport {
public:
LSPTCPTransport(unsigned short _port, Trace _traceLevel, std::function<void(std::string_view)>);
void setTraceLevel(Trace _traceLevel) override;
bool closed() const noexcept override;
std::optional<Json::Value> receive() override;
void notify(std::string const& _method, Json::Value const& _params) override;
void reply(lsp::MessageID _id, Json::Value const& _result) override;
void error(lsp::MessageID _id, lsp::ErrorCode _code, std::string const& _message) override;
private:
boost::asio::io_service m_io_service;
boost::asio::ip::tcp::endpoint m_endpoint;
boost::asio::ip::tcp::acceptor m_acceptor;
std::optional<boost::asio::ip::tcp::iostream> m_stream;
std::optional<lsp::JSONTransport> m_jsonTransport;
Trace m_traceLevel;
std::function<void(std::string_view)> m_trace;
};
} // end namespace

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
/// Some Error type E.
error E(uint, uint);
enum Weather {
Sunny,
Cloudy,
Rainy
}
/// My contract MyContract.
///
contract MyContract
{
Weather lastWeather = Weather.Rainy;
uint constant fixedValue = 1234;
constructor()
{
}
/// Sum is summing two args and returning the result
///
/// @param a me it is
/// @param b me it is also
function sum(uint a, uint b) public pure returns (uint result)
{
Weather weather = Weather.Sunny;
uint foo = 12345 + fixedValue;
if (a == b)
revert E(a, b);
weather = Weather.Cloudy;
result = a + b + foo;
}
function main() public pure returns (uint)
{
return sum(2, 3 - 123 + 456);
}
}
contract D
{
function main() public payable returns (uint)
{
MyContract c = new MyContract();
return c.sum(2, 3);
}
}

View File

@ -0,0 +1,32 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
/// Some Error type E.
error E(uint, uint);
enum Weather {
Sunny,
Cloudy,
Rainy
}
contract MyContract
{
Weather lastWheather = Weather.Rainy;
uint constant someFixedValue = 1234;
function sum(uint a, uint b) public pure returns (uint result)
{
Weather weather = Weather.Sunny;
uint foo = 12345 + someFixedValue;
if (a == b)
revert E(a, b);
weather = Weather.Cloudy;
result = a + b + foo;
}
function main() public pure returns (uint)
{
return sum(2, 3 - 123 + 456);
}
}

View File

@ -157,7 +157,7 @@ BOOST_AUTO_TEST_CASE(multiple_input_modes)
};
string expectedMessage =
"The following options are mutually exclusive: "
"--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. "
"--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast, --lsp. "
"Select at most one.\n";
for (string const& mode1: inputModeOptions)

View File

@ -463,6 +463,8 @@ int main(int argc, char const *argv[])
return 1;
}
// TODO(pr): Iterate here also through all LSP test cases
cout << endl << "Summary: ";
AnsiColorized(cout, !options.noColor, {BOLD, global_stats ? GREEN : RED}) <<
global_stats.successCount << "/" << global_stats.testCount;