mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Adds initial support for the Language Server Protocol (via --lsp CLI option).
This commit is contained in:
parent
7f3e3584fc
commit
702620151f
@ -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
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
70
libsolidity/lsp/LSPTypes.h
Normal file
70
libsolidity/lsp/LSPTypes.h
Normal 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.
|
||||
};
|
||||
|
||||
}
|
740
libsolidity/lsp/LanguageServer.cpp
Normal file
740
libsolidity/lsp/LanguageServer.cpp
Normal 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;
|
||||
}
|
164
libsolidity/lsp/LanguageServer.h
Normal file
164
libsolidity/lsp/LanguageServer.h
Normal 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
|
||||
|
106
libsolidity/lsp/ReferenceCollector.cpp
Normal file
106
libsolidity/lsp/ReferenceCollector.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
53
libsolidity/lsp/ReferenceCollector.h
Normal file
53
libsolidity/lsp/ReferenceCollector.h
Normal 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
|
289
libsolidity/lsp/SemanticTokensBuilder.cpp
Normal file
289
libsolidity/lsp/SemanticTokensBuilder.cpp
Normal 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
|
122
libsolidity/lsp/SemanticTokensBuilder.h
Normal file
122
libsolidity/lsp/SemanticTokensBuilder.h
Normal 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
|
138
libsolidity/lsp/Transport.cpp
Normal file
138
libsolidity/lsp/Transport.cpp
Normal 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
105
libsolidity/lsp/Transport.h
Normal 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
410
scripts/test_solidity_lsp.py
Executable 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()
|
@ -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)
|
||||
|
||||
|
@ -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, "");
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
115
solc/LSPTCPTransport.cpp
Normal 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
49
solc/LSPTCPTransport.h
Normal 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
|
51
test/libsolidity/lsp/test.sol
Normal file
51
test/libsolidity/lsp/test.sol
Normal 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);
|
||||
}
|
||||
}
|
32
test/libsolidity/lsp/test_definition.sol
Normal file
32
test/libsolidity/lsp/test_definition.sol
Normal 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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user