mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Merge pull request #12468 from ethereum/allowAllLSP
Always allow full filesystem access to LSP.
This commit is contained in:
commit
59e054bb9b
@ -1280,6 +1280,15 @@ jobs:
|
||||
- run:
|
||||
name: "Run soltest"
|
||||
command: .circleci/soltest.ps1
|
||||
- run:
|
||||
name: Install LSP test dependencies
|
||||
command: python -m pip install --user deepdiff colorama
|
||||
- run:
|
||||
name: Inspect lsp.py
|
||||
command: Get-Content ./test/lsp.py
|
||||
- run:
|
||||
name: Executing solc LSP test suite
|
||||
command: python ./test/lsp.py .\build\solc\Release\solc.exe
|
||||
- store_test_results: *store_test_results
|
||||
- store_artifacts: *artifacts_test_results
|
||||
- gitter_notify_failure_unless_pr
|
||||
|
@ -8,6 +8,7 @@ Compiler Features:
|
||||
* Assembly-Json: Export: Include source list in `sourceList` field.
|
||||
* Commandline Interface: option ``--pretty-json`` works also with the following options: ``--abi``, ``--asm-json``, ``--ast-compact-json``, ``--devdoc``, ``--storage-layout``, ``--userdoc``.
|
||||
* SMTChecker: Support ``abi.encodeCall`` taking into account the called selector.
|
||||
* Language Server: Allow full filesystem access to language server.
|
||||
|
||||
|
||||
Bugfixes:
|
||||
|
@ -176,3 +176,4 @@ set(sources
|
||||
|
||||
add_library(solidity ${sources})
|
||||
target_link_libraries(solidity PUBLIC yul evmasm langutil smtutil solutil Boost::boost fmt::fmt-header-only)
|
||||
|
||||
|
@ -17,48 +17,130 @@
|
||||
// SPDX-License-Identifier: GPL-3.0
|
||||
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
#include <libsolidity/lsp/Utils.h>
|
||||
|
||||
#include <libsolutil/StringUtils.h>
|
||||
#include <libsolutil/CommonIO.h>
|
||||
|
||||
#include <range/v3/range/conversion.hpp>
|
||||
#include <range/v3/view/transform.hpp>
|
||||
|
||||
#include <regex>
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity;
|
||||
using namespace solidity::lsp;
|
||||
using namespace solidity::frontend;
|
||||
|
||||
namespace
|
||||
{
|
||||
using solidity::util::readFileAsString;
|
||||
using solidity::util::joinHumanReadable;
|
||||
|
||||
string stripFilePrefix(string const& _path)
|
||||
FileRepository::FileRepository(boost::filesystem::path _basePath): m_basePath(std::move(_basePath))
|
||||
{
|
||||
if (_path.find("file://") == 0)
|
||||
return _path.substr(7);
|
||||
else
|
||||
return _path;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const
|
||||
string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const
|
||||
{
|
||||
if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName))
|
||||
return m_sourceUnitNamesToClientPaths.at(_sourceUnitName);
|
||||
regex const windowsDriveLetterPath("^[a-zA-Z]:/");
|
||||
|
||||
if (m_sourceUnitNamesToUri.count(_sourceUnitName))
|
||||
return m_sourceUnitNamesToUri.at(_sourceUnitName);
|
||||
else if (_sourceUnitName.find("file://") == 0)
|
||||
return _sourceUnitName;
|
||||
else if (regex_search(_sourceUnitName, windowsDriveLetterPath))
|
||||
return "file:///" + _sourceUnitName;
|
||||
else if (_sourceUnitName.find("/") == 0)
|
||||
return "file://" + _sourceUnitName;
|
||||
else
|
||||
return "file://" + (m_fileReader.basePath() / _sourceUnitName).generic_string();
|
||||
return "file://" + m_basePath.generic_string() + "/" + _sourceUnitName;
|
||||
}
|
||||
|
||||
string FileRepository::clientPathToSourceUnitName(string const& _path) const
|
||||
string FileRepository::uriToSourceUnitName(string const& _path) const
|
||||
{
|
||||
return m_fileReader.cliPathToSourceUnitName(stripFilePrefix(_path));
|
||||
return stripFileUriSchemePrefix(_path);
|
||||
}
|
||||
|
||||
map<string, string> const& FileRepository::sourceUnits() const
|
||||
{
|
||||
return m_fileReader.sourceUnits();
|
||||
}
|
||||
|
||||
void FileRepository::setSourceByClientPath(string const& _uri, string _text)
|
||||
void FileRepository::setSourceByUri(string const& _uri, string _source)
|
||||
{
|
||||
// This is needed for uris outside the base path. It can lead to collisions,
|
||||
// but we need to mostly rewrite this in a future version anyway.
|
||||
m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri);
|
||||
m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text));
|
||||
auto sourceUnitName = uriToSourceUnitName(_uri);
|
||||
m_sourceUnitNamesToUri.emplace(sourceUnitName, _uri);
|
||||
m_sourceCodes[sourceUnitName] = std::move(_source);
|
||||
}
|
||||
|
||||
frontend::ReadCallback::Result FileRepository::readFile(string const& _kind, string const& _sourceUnitName)
|
||||
{
|
||||
solAssert(
|
||||
_kind == ReadCallback::kindString(ReadCallback::Kind::ReadFile),
|
||||
"ReadFile callback used as callback kind " + _kind
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
// File was read already. Use local store.
|
||||
if (m_sourceCodes.count(_sourceUnitName))
|
||||
return ReadCallback::Result{true, m_sourceCodes.at(_sourceUnitName)};
|
||||
|
||||
string const strippedSourceUnitName = stripFileUriSchemePrefix(_sourceUnitName);
|
||||
|
||||
if (
|
||||
boost::filesystem::path(strippedSourceUnitName).has_root_path() &&
|
||||
boost::filesystem::exists(strippedSourceUnitName)
|
||||
)
|
||||
{
|
||||
auto contents = readFileAsString(strippedSourceUnitName);
|
||||
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
|
||||
m_sourceCodes[_sourceUnitName] = contents;
|
||||
return ReadCallback::Result{true, move(contents)};
|
||||
}
|
||||
|
||||
vector<boost::filesystem::path> candidates;
|
||||
vector<reference_wrapper<boost::filesystem::path>> prefixes = {m_basePath};
|
||||
prefixes += (m_includePaths | ranges::to<vector<reference_wrapper<boost::filesystem::path>>>);
|
||||
|
||||
auto const pathToQuotedString = [](boost::filesystem::path const& _path) { return "\"" + _path.string() + "\""; };
|
||||
|
||||
for (auto const& prefix: prefixes)
|
||||
{
|
||||
boost::filesystem::path canonicalPath = boost::filesystem::path(prefix) / boost::filesystem::path(strippedSourceUnitName);
|
||||
|
||||
if (boost::filesystem::exists(canonicalPath))
|
||||
candidates.push_back(move(canonicalPath));
|
||||
}
|
||||
|
||||
if (candidates.empty())
|
||||
return ReadCallback::Result{
|
||||
false,
|
||||
"File not found. Searched the following locations: " +
|
||||
joinHumanReadable(prefixes | ranges::views::transform(pathToQuotedString), ", ") +
|
||||
"."
|
||||
};
|
||||
|
||||
if (candidates.size() >= 2)
|
||||
return ReadCallback::Result{
|
||||
false,
|
||||
"Ambiguous import. "
|
||||
"Multiple matching files found inside base path and/or include paths: " +
|
||||
joinHumanReadable(candidates | ranges::views::transform(pathToQuotedString), ", ") +
|
||||
"."
|
||||
};
|
||||
|
||||
if (!boost::filesystem::is_regular_file(candidates[0]))
|
||||
return ReadCallback::Result{false, "Not a valid file."};
|
||||
|
||||
auto contents = readFileAsString(candidates[0]);
|
||||
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
|
||||
m_sourceCodes[_sourceUnitName] = contents;
|
||||
return ReadCallback::Result{true, move(contents)};
|
||||
}
|
||||
catch (std::exception const& _exception)
|
||||
{
|
||||
return ReadCallback::Result{false, "Exception in read callback: " + boost::diagnostic_information(_exception)};
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return ReadCallback::Result{false, "Unknown exception in read callback: " + boost::current_exception_diagnostic_information()};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,26 +28,42 @@ namespace solidity::lsp
|
||||
class FileRepository
|
||||
{
|
||||
public:
|
||||
explicit FileRepository(boost::filesystem::path const& _basePath):
|
||||
m_fileReader(_basePath) {}
|
||||
explicit FileRepository(boost::filesystem::path _basePath);
|
||||
|
||||
boost::filesystem::path const& basePath() const { return m_fileReader.basePath(); }
|
||||
boost::filesystem::path const& basePath() const { return m_basePath; }
|
||||
|
||||
/// Translates a compiler-internal source unit name to an LSP client path.
|
||||
std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const;
|
||||
/// Translates an LSP client path into a compiler-internal source unit name.
|
||||
std::string clientPathToSourceUnitName(std::string const& _uri) const;
|
||||
std::string sourceUnitNameToUri(std::string const& _sourceUnitName) const;
|
||||
|
||||
/// Translates an LSP file URI into a compiler-internal source unit name.
|
||||
std::string uriToSourceUnitName(std::string const& _uri) const;
|
||||
|
||||
/// @returns all sources by their compiler-internal source unit name.
|
||||
std::map<std::string, std::string> const& sourceUnits() const;
|
||||
/// Changes the source identified by the LSP client path _uri to _text.
|
||||
void setSourceByClientPath(std::string const& _uri, std::string _text);
|
||||
StringMap const& sourceUnits() const noexcept { return m_sourceCodes; }
|
||||
|
||||
frontend::ReadCallback::Callback reader() { return m_fileReader.reader(); }
|
||||
/// Changes the source identified by the LSP client path _uri to _text.
|
||||
void setSourceByUri(std::string const& _uri, std::string _text);
|
||||
|
||||
void addOrUpdateFile(boost::filesystem::path const& _path, frontend::SourceCode _source);
|
||||
void setSourceUnits(StringMap _sources);
|
||||
frontend::ReadCallback::Result readFile(std::string const& _kind, std::string const& _sourceUnitName);
|
||||
frontend::ReadCallback::Callback reader()
|
||||
{
|
||||
return [this](std::string const& _kind, std::string const& _path) { return readFile(_kind, _path); };
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> m_sourceUnitNamesToClientPaths;
|
||||
frontend::FileReader m_fileReader;
|
||||
/// Base path without URI scheme.
|
||||
boost::filesystem::path m_basePath;
|
||||
|
||||
/// Additional directories used for resolving relative paths in imports.
|
||||
std::vector<boost::filesystem::path> m_includePaths;
|
||||
|
||||
/// Mapping of source unit names to their URIs as understood by the client.
|
||||
StringMap m_sourceUnitNamesToUri;
|
||||
|
||||
/// Mapping of source unit names to their file content.
|
||||
StringMap m_sourceCodes;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
|
||||
{
|
||||
solAssert(_location.sourceName);
|
||||
Json::Value item = Json::objectValue;
|
||||
item["uri"] = fileRepository().sourceUnitNameToClientPath(*_location.sourceName);
|
||||
item["uri"] = fileRepository().sourceUnitNameToUri(*_location.sourceName);
|
||||
item["range"] = toRange(_location);
|
||||
return item;
|
||||
}
|
||||
@ -55,7 +55,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
|
||||
pair<string, LineColumn> HandlerBase::extractSourceUnitNameAndLineColumn(Json::Value const& _args) const
|
||||
{
|
||||
string const uri = _args["textDocument"]["uri"].asString();
|
||||
string const sourceUnitName = fileRepository().clientPathToSourceUnitName(uri);
|
||||
string const sourceUnitName = fileRepository().uriToSourceUnitName(uri);
|
||||
if (!fileRepository().sourceUnits().count(sourceUnitName))
|
||||
BOOST_THROW_EXCEPTION(
|
||||
RequestError(ErrorCode::RequestFailed) <<
|
||||
|
@ -37,8 +37,6 @@
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
@ -114,9 +112,9 @@ void LanguageServer::compile()
|
||||
swap(oldRepository, m_fileRepository);
|
||||
|
||||
for (string const& fileName: m_openFiles)
|
||||
m_fileRepository.setSourceByClientPath(
|
||||
m_fileRepository.setSourceByUri(
|
||||
fileName,
|
||||
oldRepository.sourceUnits().at(oldRepository.clientPathToSourceUnitName(fileName))
|
||||
oldRepository.sourceUnits().at(oldRepository.uriToSourceUnitName(fileName))
|
||||
);
|
||||
|
||||
// TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty).
|
||||
@ -178,7 +176,7 @@ void LanguageServer::compileAndUpdateDiagnostics()
|
||||
for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit)
|
||||
{
|
||||
Json::Value params;
|
||||
params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName);
|
||||
params["uri"] = m_fileRepository.sourceUnitNameToUri(sourceUnitName);
|
||||
if (!diagnostics.empty())
|
||||
m_nonemptyDiagnostics.insert(sourceUnitName);
|
||||
params["diagnostics"] = move(diagnostics);
|
||||
@ -252,13 +250,12 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
|
||||
ErrorCode::InvalidParams,
|
||||
"rootUri only supports file URI scheme."
|
||||
);
|
||||
|
||||
rootPath = rootPath.substr(7);
|
||||
rootPath = stripFileUriSchemePrefix(rootPath);
|
||||
}
|
||||
else if (Json::Value rootPath = _args["rootPath"])
|
||||
rootPath = rootPath.asString();
|
||||
|
||||
m_fileRepository = FileRepository(boost::filesystem::path(rootPath));
|
||||
m_fileRepository = FileRepository(rootPath);
|
||||
if (_args["initializationOptions"].isObject())
|
||||
changeConfiguration(_args["initializationOptions"]);
|
||||
|
||||
@ -309,7 +306,7 @@ void LanguageServer::handleTextDocumentDidOpen(Json::Value const& _args)
|
||||
string text = _args["textDocument"]["text"].asString();
|
||||
string uri = _args["textDocument"]["uri"].asString();
|
||||
m_openFiles.insert(uri);
|
||||
m_fileRepository.setSourceByClientPath(uri, move(text));
|
||||
m_fileRepository.setSourceByUri(uri, move(text));
|
||||
compileAndUpdateDiagnostics();
|
||||
}
|
||||
|
||||
@ -327,7 +324,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args)
|
||||
"Invalid content reference."
|
||||
);
|
||||
|
||||
string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri);
|
||||
string const sourceUnitName = m_fileRepository.uriToSourceUnitName(uri);
|
||||
lspAssert(
|
||||
m_fileRepository.sourceUnits().count(sourceUnitName),
|
||||
ErrorCode::RequestFailed,
|
||||
@ -348,7 +345,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args)
|
||||
buffer.replace(static_cast<size_t>(change->start), static_cast<size_t>(change->end - change->start), move(text));
|
||||
text = move(buffer);
|
||||
}
|
||||
m_fileRepository.setSourceByClientPath(uri, move(text));
|
||||
m_fileRepository.setSourceByUri(uri, move(text));
|
||||
}
|
||||
|
||||
compileAndUpdateDiagnostics();
|
||||
|
@ -92,7 +92,7 @@ private:
|
||||
Transport& m_client;
|
||||
std::map<std::string, MessageHandler> m_handlers;
|
||||
|
||||
/// Set of files known to be open by the client.
|
||||
/// Set of files (names in URI form) known to be open by the client.
|
||||
std::set<std::string> m_openFiles;
|
||||
/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
|
||||
std::set<std::string> m_nonemptyDiagnostics;
|
||||
|
@ -22,31 +22,25 @@
|
||||
#include <libsolutil/CommonIO.h>
|
||||
#include <liblangutil/Exceptions.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <io.h>
|
||||
#include <fcntl.h>
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
using namespace solidity::lsp;
|
||||
|
||||
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out):
|
||||
m_input{_in},
|
||||
m_output{_out}
|
||||
{
|
||||
}
|
||||
|
||||
IOStreamTransport::IOStreamTransport():
|
||||
IOStreamTransport(cin, cout)
|
||||
{
|
||||
}
|
||||
|
||||
bool IOStreamTransport::closed() const noexcept
|
||||
{
|
||||
return m_input.eof();
|
||||
}
|
||||
|
||||
optional<Json::Value> IOStreamTransport::receive()
|
||||
// {{{ Transport
|
||||
optional<Json::Value> Transport::receive()
|
||||
{
|
||||
auto const headers = parseHeaders();
|
||||
if (!headers)
|
||||
@ -61,7 +55,7 @@ optional<Json::Value> IOStreamTransport::receive()
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
string const data = util::readBytes(m_input, stoul(headers->at("content-length")));
|
||||
string const data = readBytes(stoul(headers->at("content-length")));
|
||||
|
||||
Json::Value jsonMessage;
|
||||
string jsonParsingErrors;
|
||||
@ -75,29 +69,6 @@ optional<Json::Value> IOStreamTransport::receive()
|
||||
return {move(jsonMessage)};
|
||||
}
|
||||
|
||||
void IOStreamTransport::notify(string _method, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["method"] = move(_method);
|
||||
json["params"] = move(_message);
|
||||
send(move(json));
|
||||
}
|
||||
|
||||
void IOStreamTransport::reply(MessageID _id, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["result"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void IOStreamTransport::error(MessageID _id, ErrorCode _code, string _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["error"]["code"] = static_cast<int>(_code);
|
||||
json["error"]["message"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void Transport::trace(std::string _message, Json::Value _extra)
|
||||
{
|
||||
if (m_logTrace != TraceValue::Off)
|
||||
@ -110,30 +81,13 @@ void Transport::trace(std::string _message, Json::Value _extra)
|
||||
}
|
||||
}
|
||||
|
||||
void IOStreamTransport::send(Json::Value _json, MessageID _id)
|
||||
{
|
||||
solAssert(_json.isObject());
|
||||
_json["jsonrpc"] = "2.0";
|
||||
if (_id != Json::nullValue)
|
||||
_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();
|
||||
}
|
||||
|
||||
optional<map<string, string>> IOStreamTransport::parseHeaders()
|
||||
optional<map<string, string>> Transport::parseHeaders()
|
||||
{
|
||||
map<string, string> headers;
|
||||
|
||||
while (true)
|
||||
{
|
||||
string line;
|
||||
getline(m_input, line);
|
||||
auto line = getline();
|
||||
if (boost::trim_copy(line).empty())
|
||||
break;
|
||||
|
||||
@ -141,13 +95,127 @@ optional<map<string, string>> IOStreamTransport::parseHeaders()
|
||||
if (delimiterPos == string::npos)
|
||||
return nullopt;
|
||||
|
||||
string name = boost::to_lower_copy(line.substr(0, delimiterPos));
|
||||
string value = line.substr(delimiterPos + 1);
|
||||
if (!headers.emplace(
|
||||
boost::trim_copy(name),
|
||||
boost::trim_copy(value)
|
||||
).second)
|
||||
auto const name = boost::to_lower_copy(line.substr(0, delimiterPos));
|
||||
auto const value = line.substr(delimiterPos + 1);
|
||||
if (!headers.emplace(boost::trim_copy(name), boost::trim_copy(value)).second)
|
||||
return nullopt;
|
||||
}
|
||||
return {move(headers)};
|
||||
}
|
||||
|
||||
void Transport::notify(string _method, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["method"] = move(_method);
|
||||
json["params"] = move(_message);
|
||||
send(move(json));
|
||||
}
|
||||
|
||||
void Transport::reply(MessageID _id, Json::Value _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["result"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void Transport::error(MessageID _id, ErrorCode _code, string _message)
|
||||
{
|
||||
Json::Value json;
|
||||
json["error"]["code"] = static_cast<int>(_code);
|
||||
json["error"]["message"] = move(_message);
|
||||
send(move(json), _id);
|
||||
}
|
||||
|
||||
void Transport::send(Json::Value _json, MessageID _id)
|
||||
{
|
||||
solAssert(_json.isObject());
|
||||
_json["jsonrpc"] = "2.0";
|
||||
if (_id != Json::nullValue)
|
||||
_json["id"] = _id;
|
||||
|
||||
// Trailing CRLF only for easier readability.
|
||||
string const jsonString = solidity::util::jsonCompactPrint(_json);
|
||||
|
||||
writeBytes(fmt::format("Content-Length: {}\r\n\r\n", jsonString.size()));
|
||||
writeBytes(jsonString);
|
||||
flushOutput();
|
||||
}
|
||||
// }}}
|
||||
|
||||
// {{{ IOStreamTransport
|
||||
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out):
|
||||
m_input{_in},
|
||||
m_output{_out}
|
||||
{
|
||||
}
|
||||
|
||||
bool IOStreamTransport::closed() const noexcept
|
||||
{
|
||||
return m_input.eof();
|
||||
}
|
||||
|
||||
std::string IOStreamTransport::readBytes(size_t _length)
|
||||
{
|
||||
return util::readBytes(m_input, _length);
|
||||
}
|
||||
|
||||
std::string IOStreamTransport::getline()
|
||||
{
|
||||
string line;
|
||||
std::getline(m_input, line);
|
||||
return line;
|
||||
}
|
||||
|
||||
void IOStreamTransport::writeBytes(std::string_view _data)
|
||||
{
|
||||
m_output.write(_data.data(), static_cast<std::streamsize>(_data.size()));
|
||||
}
|
||||
|
||||
void IOStreamTransport::flushOutput()
|
||||
{
|
||||
m_output.flush();
|
||||
}
|
||||
// }}}
|
||||
|
||||
// {{{ StdioTransport
|
||||
StdioTransport::StdioTransport()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
// Attempt to change the modes of stdout from text to binary.
|
||||
setmode(fileno(stdout), O_BINARY);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool StdioTransport::closed() const noexcept
|
||||
{
|
||||
return feof(stdin);
|
||||
}
|
||||
|
||||
std::string StdioTransport::readBytes(size_t _byteCount)
|
||||
{
|
||||
std::string buffer;
|
||||
buffer.resize(_byteCount);
|
||||
auto const n = fread(buffer.data(), 1, _byteCount, stdin);
|
||||
if (n < _byteCount)
|
||||
buffer.resize(n);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string StdioTransport::getline()
|
||||
{
|
||||
std::string line;
|
||||
std::getline(std::cin, line);
|
||||
return line;
|
||||
}
|
||||
|
||||
void StdioTransport::writeBytes(std::string_view _data)
|
||||
{
|
||||
auto const bytesWritten = fwrite(_data.data(), 1, _data.size(), stdout);
|
||||
solAssert(bytesWritten == _data.size());
|
||||
}
|
||||
|
||||
void StdioTransport::flushOutput()
|
||||
{
|
||||
fflush(stdout);
|
||||
}
|
||||
// }}}
|
||||
|
@ -91,20 +91,44 @@ class Transport
|
||||
public:
|
||||
virtual ~Transport() = default;
|
||||
|
||||
std::optional<Json::Value> receive();
|
||||
void notify(std::string _method, Json::Value _params);
|
||||
void reply(MessageID _id, Json::Value _result);
|
||||
void error(MessageID _id, ErrorCode _code, std::string _message);
|
||||
|
||||
virtual bool closed() const noexcept = 0;
|
||||
virtual std::optional<Json::Value> receive() = 0;
|
||||
virtual void notify(std::string _method, Json::Value _params) = 0;
|
||||
virtual void reply(MessageID _id, Json::Value _result) = 0;
|
||||
virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0;
|
||||
|
||||
void trace(std::string _message, Json::Value _extra = Json::nullValue);
|
||||
|
||||
TraceValue traceValue() const noexcept { return m_logTrace; }
|
||||
void setTrace(TraceValue _value) noexcept { m_logTrace = _value; }
|
||||
|
||||
|
||||
private:
|
||||
TraceValue m_logTrace = TraceValue::Off;
|
||||
|
||||
protected:
|
||||
/// Reads from the transport and parses the headers until the beginning
|
||||
/// of the contents.
|
||||
std::optional<std::map<std::string, std::string>> parseHeaders();
|
||||
|
||||
/// Consumes exactly @p _byteCount bytes, as needed for consuming
|
||||
/// the message body from the transport line.
|
||||
virtual std::string readBytes(size_t _byteCount) = 0;
|
||||
|
||||
// Mimmicks std::getline() on this Transport API.
|
||||
virtual std::string getline() = 0;
|
||||
|
||||
/// Writes the given payload @p _data to transport.
|
||||
/// This call may or may not buffer.
|
||||
virtual void writeBytes(std::string_view _data) = 0;
|
||||
|
||||
/// Ensures transport output is flushed.
|
||||
virtual void flushOutput() = 0;
|
||||
|
||||
/// 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);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -119,27 +143,34 @@ public:
|
||||
/// @param _out for example std::cout (stdout)
|
||||
IOStreamTransport(std::istream& _in, std::ostream& _out);
|
||||
|
||||
// Constructs a JSON transport using standard I/O streams.
|
||||
IOStreamTransport();
|
||||
|
||||
bool closed() const noexcept override;
|
||||
std::optional<Json::Value> receive() override;
|
||||
void notify(std::string _method, Json::Value _params) override;
|
||||
void reply(MessageID _id, Json::Value _result) override;
|
||||
void error(MessageID _id, ErrorCode _code, std::string _message) override;
|
||||
|
||||
protected:
|
||||
/// 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();
|
||||
std::string readBytes(size_t _byteCount) override;
|
||||
std::string getline() override;
|
||||
void writeBytes(std::string_view _data) override;
|
||||
void flushOutput() override;
|
||||
|
||||
private:
|
||||
std::istream& m_input;
|
||||
std::ostream& m_output;
|
||||
};
|
||||
|
||||
/**
|
||||
* Standard I/O transport Layer utilizing stdin/stdout for communication.
|
||||
*/
|
||||
class StdioTransport: public Transport
|
||||
{
|
||||
public:
|
||||
StdioTransport();
|
||||
|
||||
bool closed() const noexcept override;
|
||||
|
||||
protected:
|
||||
std::string readBytes(size_t _byteCount) override;
|
||||
std::string getline() override;
|
||||
void writeBytes(std::string_view _data) override;
|
||||
void flushOutput() override;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
#include <libsolidity/lsp/FileRepository.h>
|
||||
#include <libsolidity/lsp/Utils.h>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
|
||||
namespace solidity::lsp
|
||||
@ -115,4 +115,15 @@ optional<SourceLocation> parseRange(FileRepository const& _fileRepository, strin
|
||||
return start;
|
||||
}
|
||||
|
||||
string stripFileUriSchemePrefix(string const& _path)
|
||||
{
|
||||
regex const windowsDriveLetterPath("^file:///[a-zA-Z]:/");
|
||||
if (regex_search(_path, windowsDriveLetterPath))
|
||||
return _path.substr(8);
|
||||
if (_path.find("file://") == 0)
|
||||
return _path.substr(7);
|
||||
else
|
||||
return _path;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,6 +64,13 @@ std::optional<langutil::SourceLocation> parseRange(
|
||||
Json::Value const& _range
|
||||
);
|
||||
|
||||
/// Strips the file:// URI prefix off the given path, if present,
|
||||
/// also taking special care of Windows-drive-letter paths.
|
||||
///
|
||||
/// So file:///path/to/some/file.txt returns /path/to/some/file.txt, as well as,
|
||||
/// file:///C:/file.txt will return C:/file.txt (forward-slash is okay on Windows).
|
||||
std::string stripFileUriSchemePrefix(std::string const& _path);
|
||||
|
||||
/// Extracts the resolved declaration of the given expression AST node.
|
||||
///
|
||||
/// This may for example be the type declaration of an identifier,
|
||||
|
@ -912,7 +912,7 @@ void CommandLineInterface::handleAst()
|
||||
|
||||
void CommandLineInterface::serveLSP()
|
||||
{
|
||||
lsp::IOStreamTransport transport;
|
||||
lsp::StdioTransport transport;
|
||||
if (!lsp::LanguageServer{transport}.run())
|
||||
solThrow(CommandLineExecutionError, "LSP terminated abnormally.");
|
||||
}
|
||||
|
79
test/lsp.py
79
test/lsp.py
@ -1,25 +1,57 @@
|
||||
#!/usr/bin/env python3
|
||||
# pragma pylint: disable=too-many-lines
|
||||
# test line 1
|
||||
import argparse
|
||||
import fnmatch
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
import re
|
||||
import tty
|
||||
import functools
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
from itertools import islice
|
||||
|
||||
from enum import Enum, auto
|
||||
from itertools import islice
|
||||
from pathlib import PurePath
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
|
||||
import colorama # Enables the use of SGR & CUP terminal VT sequences on Windows.
|
||||
import colorama # Enables the use of SGR & CUP terminal VT sequences on Windows.
|
||||
from deepdiff import DeepDiff
|
||||
|
||||
if os.name == 'nt':
|
||||
# pragma pylint: disable=import-error
|
||||
import msvcrt
|
||||
else:
|
||||
import tty
|
||||
# Turn off user input buffering so we get the input immediately,
|
||||
# not only after a line break
|
||||
tty.setcbreak(sys.stdin.fileno())
|
||||
|
||||
|
||||
def escape_string(text: str) -> str:
|
||||
"""
|
||||
Trivially escapes given input string's \r \n and \\.
|
||||
"""
|
||||
return text.translate(str.maketrans({
|
||||
"\r": r"\r",
|
||||
"\n": r"\n",
|
||||
"\\": r"\\"
|
||||
}))
|
||||
|
||||
|
||||
def getCharFromStdin():
|
||||
"""
|
||||
Gets a single character from stdin without line-buffering.
|
||||
"""
|
||||
if os.name == 'nt':
|
||||
# pragma pylint: disable=import-error
|
||||
return msvcrt.getch().decode("utf-8")
|
||||
else:
|
||||
return sys.stdin.buffer.read(1)
|
||||
|
||||
|
||||
"""
|
||||
Named tuple that holds various regexes used to parse the test specification.
|
||||
"""
|
||||
@ -100,7 +132,6 @@ class BadHeader(Exception):
|
||||
def __init__(self, msg: str):
|
||||
super().__init__("Bad header: " + msg)
|
||||
|
||||
|
||||
class JsonRpcProcess:
|
||||
exe_path: str
|
||||
exe_args: List[str]
|
||||
@ -143,10 +174,12 @@ class JsonRpcProcess:
|
||||
# server quit
|
||||
return None
|
||||
line = line.decode("utf-8")
|
||||
if self.trace_io:
|
||||
print(f"Received header-line: {escape_string(line)}")
|
||||
if not line.endswith("\r\n"):
|
||||
raise BadHeader("missing newline")
|
||||
# remove the "\r\n"
|
||||
line = line[:-2]
|
||||
# Safely remove the "\r\n".
|
||||
line = line.rstrip("\r\n")
|
||||
if line == '':
|
||||
break # done with the headers
|
||||
if line.startswith(CONTENT_LENGTH_HEADER):
|
||||
@ -588,7 +621,7 @@ class FileTestRunner:
|
||||
|
||||
while True:
|
||||
print("(u)pdate/(r)etry/(i)gnore?")
|
||||
user_response = sys.stdin.read(1)
|
||||
user_response = getCharFromStdin()
|
||||
if user_response == "i":
|
||||
return self.TestResult.SuccessOrIgnored
|
||||
|
||||
@ -694,7 +727,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
args = create_cli_parser().parse_args()
|
||||
self.solc_path = args.solc_path
|
||||
self.project_root_dir = os.path.realpath(args.project_root_dir) + "/test/libsolidity/lsp"
|
||||
self.project_root_uri = "file://" + self.project_root_dir
|
||||
self.project_root_uri = PurePath(self.project_root_dir).as_uri()
|
||||
self.print_assertions = args.print_assertions
|
||||
self.trace_io = args.trace_io
|
||||
self.test_pattern = args.test_pattern
|
||||
@ -777,7 +810,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
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)
|
||||
return PurePath(self.get_test_file_path(test_case_name)).as_uri()
|
||||
|
||||
def get_test_file_contents(self, test_case_name):
|
||||
"""
|
||||
@ -786,7 +819,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
in the test path (test/libsolidity/lsp).
|
||||
"""
|
||||
with open(self.get_test_file_path(test_case_name), mode="r", encoding="utf-8", newline='') as f:
|
||||
return f.read()
|
||||
return f.read().replace("\r\n", "\n")
|
||||
|
||||
def require_params_for_method(self, method_name: str, message: dict) -> Any:
|
||||
"""
|
||||
@ -1058,7 +1091,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
"""
|
||||
while True:
|
||||
print("(u)pdate/(r)etry/(s)kip file?")
|
||||
user_response = sys.stdin.read(1)
|
||||
user_response = getCharFromStdin()
|
||||
if user_response == "u":
|
||||
while True:
|
||||
try:
|
||||
@ -1067,8 +1100,8 @@ class SolidityLSPTestSuite: # {{{
|
||||
# pragma pylint: disable=broad-except
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if ret := self.user_interaction_failed_autoupdate(test):
|
||||
return ret
|
||||
if self.user_interaction_failed_autoupdate(test):
|
||||
return True
|
||||
elif user_response == 's':
|
||||
return True
|
||||
elif user_response == 'r':
|
||||
@ -1076,7 +1109,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
|
||||
def user_interaction_failed_autoupdate(self, test):
|
||||
print("(e)dit/(r)etry/(s)kip file?")
|
||||
user_response = sys.stdin.read(1)
|
||||
user_response = getCharFromStdin()
|
||||
if user_response == "r":
|
||||
print("retrying...")
|
||||
# pragma pylint: disable=no-member
|
||||
@ -1141,7 +1174,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
marker = self.get_file_tags("lib")["@diagnostics"]
|
||||
self.expect_diagnostic(report['diagnostics'][0], code=2072, marker=marker)
|
||||
|
||||
@functools.lru_cache # pragma pylint: disable=lru-cache-decorating-method
|
||||
@functools.lru_cache() # pragma pylint: disable=lru-cache-decorating-method
|
||||
def get_file_tags(self, test_name: str, verbose=False):
|
||||
"""
|
||||
Finds all tags (e.g. @tagname) in the given test and returns them as a
|
||||
@ -1438,7 +1471,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
"""
|
||||
|
||||
self.setup_lsp(solc)
|
||||
FILE_A_URI = f'file://{self.project_root_dir}/a.sol'
|
||||
FILE_A_URI = f'{self.project_root_uri}/a.sol'
|
||||
solc.send_message('textDocument/didOpen', {
|
||||
'textDocument': {
|
||||
'uri': FILE_A_URI,
|
||||
@ -1466,7 +1499,7 @@ class SolidityLSPTestSuite: # {{{
|
||||
)
|
||||
reports = self.wait_for_diagnostics(solc)
|
||||
self.expect_equal(len(reports), 1, '')
|
||||
self.expect_equal(reports[0]['uri'], f'file://{self.project_root_dir}/lib.sol', "")
|
||||
self.expect_equal(reports[0]['uri'], f'{self.project_root_uri}/lib.sol', "")
|
||||
self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics")
|
||||
|
||||
def test_textDocument_didChange_at_eol(self, solc: JsonRpcProcess) -> None:
|
||||
@ -1652,10 +1685,8 @@ class SolidityLSPTestSuite: # {{{
|
||||
# }}}
|
||||
# }}}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Turn off user input buffering so we get the input immediately,
|
||||
# not only after a line break
|
||||
tty.setcbreak(sys.stdin.fileno())
|
||||
suite = SolidityLSPTestSuite()
|
||||
exit_code = suite.main()
|
||||
sys.exit(exit_code)
|
||||
|
Loading…
Reference in New Issue
Block a user