Merge pull request #12468 from ethereum/allowAllLSP

Always allow full filesystem access to LSP.
This commit is contained in:
Christian Parpart 2022-05-09 14:20:59 +02:00 committed by GitHub
commit 59e054bb9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 414 additions and 160 deletions

View File

@ -1280,6 +1280,15 @@ jobs:
- run: - run:
name: "Run soltest" name: "Run soltest"
command: .circleci/soltest.ps1 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_test_results: *store_test_results
- store_artifacts: *artifacts_test_results - store_artifacts: *artifacts_test_results
- gitter_notify_failure_unless_pr - gitter_notify_failure_unless_pr

View File

@ -8,6 +8,7 @@ Compiler Features:
* Assembly-Json: Export: Include source list in `sourceList` field. * 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``. * 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. * SMTChecker: Support ``abi.encodeCall`` taking into account the called selector.
* Language Server: Allow full filesystem access to language server.
Bugfixes: Bugfixes:

View File

@ -176,3 +176,4 @@ set(sources
add_library(solidity ${sources}) add_library(solidity ${sources})
target_link_libraries(solidity PUBLIC yul evmasm langutil smtutil solutil Boost::boost fmt::fmt-header-only) target_link_libraries(solidity PUBLIC yul evmasm langutil smtutil solutil Boost::boost fmt::fmt-header-only)

View File

@ -17,48 +17,130 @@
// SPDX-License-Identifier: GPL-3.0 // SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/FileRepository.h> #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 std;
using namespace solidity; using namespace solidity;
using namespace solidity::lsp; 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::sourceUnitNameToUri(string const& _sourceUnitName) const
string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const
{ {
if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName)) regex const windowsDriveLetterPath("^[a-zA-Z]:/");
return m_sourceUnitNamesToClientPaths.at(_sourceUnitName);
if (m_sourceUnitNamesToUri.count(_sourceUnitName))
return m_sourceUnitNamesToUri.at(_sourceUnitName);
else if (_sourceUnitName.find("file://") == 0) else if (_sourceUnitName.find("file://") == 0)
return _sourceUnitName; return _sourceUnitName;
else if (regex_search(_sourceUnitName, windowsDriveLetterPath))
return "file:///" + _sourceUnitName;
else if (_sourceUnitName.find("/") == 0)
return "file://" + _sourceUnitName;
else 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 void FileRepository::setSourceByUri(string const& _uri, string _source)
{
return m_fileReader.sourceUnits();
}
void FileRepository::setSourceByClientPath(string const& _uri, string _text)
{ {
// This is needed for uris outside the base path. It can lead to collisions, // 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. // but we need to mostly rewrite this in a future version anyway.
m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri); auto sourceUnitName = uriToSourceUnitName(_uri);
m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text)); 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()};
}
}

View File

@ -28,26 +28,42 @@ namespace solidity::lsp
class FileRepository class FileRepository
{ {
public: public:
explicit FileRepository(boost::filesystem::path const& _basePath): explicit FileRepository(boost::filesystem::path _basePath);
m_fileReader(_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. /// Translates a compiler-internal source unit name to an LSP client path.
std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const; std::string sourceUnitNameToUri(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; /// 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. /// @returns all sources by their compiler-internal source unit name.
std::map<std::string, std::string> const& sourceUnits() const; StringMap const& sourceUnits() const noexcept { return m_sourceCodes; }
/// Changes the source identified by the LSP client path _uri to _text.
void setSourceByClientPath(std::string const& _uri, std::string _text);
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: private:
std::map<std::string, std::string> m_sourceUnitNamesToClientPaths; /// Base path without URI scheme.
frontend::FileReader m_fileReader; 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;
}; };
} }

View File

@ -47,7 +47,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
{ {
solAssert(_location.sourceName); solAssert(_location.sourceName);
Json::Value item = Json::objectValue; Json::Value item = Json::objectValue;
item["uri"] = fileRepository().sourceUnitNameToClientPath(*_location.sourceName); item["uri"] = fileRepository().sourceUnitNameToUri(*_location.sourceName);
item["range"] = toRange(_location); item["range"] = toRange(_location);
return item; return item;
} }
@ -55,7 +55,7 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const
pair<string, LineColumn> HandlerBase::extractSourceUnitNameAndLineColumn(Json::Value const& _args) const pair<string, LineColumn> HandlerBase::extractSourceUnitNameAndLineColumn(Json::Value const& _args) const
{ {
string const uri = _args["textDocument"]["uri"].asString(); string const uri = _args["textDocument"]["uri"].asString();
string const sourceUnitName = fileRepository().clientPathToSourceUnitName(uri); string const sourceUnitName = fileRepository().uriToSourceUnitName(uri);
if (!fileRepository().sourceUnits().count(sourceUnitName)) if (!fileRepository().sourceUnits().count(sourceUnitName))
BOOST_THROW_EXCEPTION( BOOST_THROW_EXCEPTION(
RequestError(ErrorCode::RequestFailed) << RequestError(ErrorCode::RequestFailed) <<

View File

@ -37,8 +37,6 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <boost/algorithm/string/predicate.hpp> #include <boost/algorithm/string/predicate.hpp>
#include <fmt/format.h>
#include <ostream> #include <ostream>
#include <string> #include <string>
@ -114,9 +112,9 @@ void LanguageServer::compile()
swap(oldRepository, m_fileRepository); swap(oldRepository, m_fileRepository);
for (string const& fileName: m_openFiles) for (string const& fileName: m_openFiles)
m_fileRepository.setSourceByClientPath( m_fileRepository.setSourceByUri(
fileName, 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). // 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) for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit)
{ {
Json::Value params; Json::Value params;
params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName); params["uri"] = m_fileRepository.sourceUnitNameToUri(sourceUnitName);
if (!diagnostics.empty()) if (!diagnostics.empty())
m_nonemptyDiagnostics.insert(sourceUnitName); m_nonemptyDiagnostics.insert(sourceUnitName);
params["diagnostics"] = move(diagnostics); params["diagnostics"] = move(diagnostics);
@ -252,13 +250,12 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
ErrorCode::InvalidParams, ErrorCode::InvalidParams,
"rootUri only supports file URI scheme." "rootUri only supports file URI scheme."
); );
rootPath = stripFileUriSchemePrefix(rootPath);
rootPath = rootPath.substr(7);
} }
else if (Json::Value rootPath = _args["rootPath"]) else if (Json::Value rootPath = _args["rootPath"])
rootPath = rootPath.asString(); rootPath = rootPath.asString();
m_fileRepository = FileRepository(boost::filesystem::path(rootPath)); m_fileRepository = FileRepository(rootPath);
if (_args["initializationOptions"].isObject()) if (_args["initializationOptions"].isObject())
changeConfiguration(_args["initializationOptions"]); changeConfiguration(_args["initializationOptions"]);
@ -309,7 +306,7 @@ void LanguageServer::handleTextDocumentDidOpen(Json::Value const& _args)
string text = _args["textDocument"]["text"].asString(); string text = _args["textDocument"]["text"].asString();
string uri = _args["textDocument"]["uri"].asString(); string uri = _args["textDocument"]["uri"].asString();
m_openFiles.insert(uri); m_openFiles.insert(uri);
m_fileRepository.setSourceByClientPath(uri, move(text)); m_fileRepository.setSourceByUri(uri, move(text));
compileAndUpdateDiagnostics(); compileAndUpdateDiagnostics();
} }
@ -327,7 +324,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args)
"Invalid content reference." "Invalid content reference."
); );
string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri); string const sourceUnitName = m_fileRepository.uriToSourceUnitName(uri);
lspAssert( lspAssert(
m_fileRepository.sourceUnits().count(sourceUnitName), m_fileRepository.sourceUnits().count(sourceUnitName),
ErrorCode::RequestFailed, 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)); buffer.replace(static_cast<size_t>(change->start), static_cast<size_t>(change->end - change->start), move(text));
text = move(buffer); text = move(buffer);
} }
m_fileRepository.setSourceByClientPath(uri, move(text)); m_fileRepository.setSourceByUri(uri, move(text));
} }
compileAndUpdateDiagnostics(); compileAndUpdateDiagnostics();

View File

@ -92,7 +92,7 @@ private:
Transport& m_client; Transport& m_client;
std::map<std::string, MessageHandler> m_handlers; 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; std::set<std::string> m_openFiles;
/// Set of source unit names for which we sent diagnostics to the client in the last iteration. /// Set of source unit names for which we sent diagnostics to the client in the last iteration.
std::set<std::string> m_nonemptyDiagnostics; std::set<std::string> m_nonemptyDiagnostics;

View File

@ -22,31 +22,25 @@
#include <libsolutil/CommonIO.h> #include <libsolutil/CommonIO.h>
#include <liblangutil/Exceptions.h> #include <liblangutil/Exceptions.h>
#include <fmt/format.h>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include <iostream> #include <iostream>
#include <sstream> #include <sstream>
#include <string>
#if defined(_WIN32)
#include <io.h>
#include <fcntl.h>
#endif
using namespace std; using namespace std;
using namespace solidity::lsp; using namespace solidity::lsp;
IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out): // {{{ Transport
m_input{_in}, optional<Json::Value> Transport::receive()
m_output{_out}
{
}
IOStreamTransport::IOStreamTransport():
IOStreamTransport(cin, cout)
{
}
bool IOStreamTransport::closed() const noexcept
{
return m_input.eof();
}
optional<Json::Value> IOStreamTransport::receive()
{ {
auto const headers = parseHeaders(); auto const headers = parseHeaders();
if (!headers) if (!headers)
@ -61,7 +55,7 @@ optional<Json::Value> IOStreamTransport::receive()
return nullopt; 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; Json::Value jsonMessage;
string jsonParsingErrors; string jsonParsingErrors;
@ -75,29 +69,6 @@ optional<Json::Value> IOStreamTransport::receive()
return {move(jsonMessage)}; 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) void Transport::trace(std::string _message, Json::Value _extra)
{ {
if (m_logTrace != TraceValue::Off) 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) optional<map<string, string>> Transport::parseHeaders()
{
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()
{ {
map<string, string> headers; map<string, string> headers;
while (true) while (true)
{ {
string line; auto line = getline();
getline(m_input, line);
if (boost::trim_copy(line).empty()) if (boost::trim_copy(line).empty())
break; break;
@ -141,13 +95,127 @@ optional<map<string, string>> IOStreamTransport::parseHeaders()
if (delimiterPos == string::npos) if (delimiterPos == string::npos)
return nullopt; return nullopt;
string name = boost::to_lower_copy(line.substr(0, delimiterPos)); auto const name = boost::to_lower_copy(line.substr(0, delimiterPos));
string value = line.substr(delimiterPos + 1); auto const value = line.substr(delimiterPos + 1);
if (!headers.emplace( if (!headers.emplace(boost::trim_copy(name), boost::trim_copy(value)).second)
boost::trim_copy(name),
boost::trim_copy(value)
).second)
return nullopt; return nullopt;
} }
return {move(headers)}; 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);
}
// }}}

View File

@ -91,20 +91,44 @@ class Transport
public: public:
virtual ~Transport() = default; 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 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); void trace(std::string _message, Json::Value _extra = Json::nullValue);
TraceValue traceValue() const noexcept { return m_logTrace; } TraceValue traceValue() const noexcept { return m_logTrace; }
void setTrace(TraceValue _value) noexcept { m_logTrace = _value; } void setTrace(TraceValue _value) noexcept { m_logTrace = _value; }
private: private:
TraceValue m_logTrace = TraceValue::Off; 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) /// @param _out for example std::cout (stdout)
IOStreamTransport(std::istream& _in, std::ostream& _out); IOStreamTransport(std::istream& _in, std::ostream& _out);
// Constructs a JSON transport using standard I/O streams.
IOStreamTransport();
bool closed() const noexcept override; 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: protected:
/// Sends an arbitrary raw message to the client. std::string readBytes(size_t _byteCount) override;
/// std::string getline() override;
/// Used by the notify/reply/error function family. void writeBytes(std::string_view _data) override;
virtual void send(Json::Value _message, MessageID _id = Json::nullValue); void flushOutput() override;
/// Parses header section from the client including message-delimiting empty line.
std::optional<std::map<std::string, std::string>> parseHeaders();
private: private:
std::istream& m_input; std::istream& m_input;
std::ostream& m_output; 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;
};
} }

View File

@ -22,7 +22,7 @@
#include <libsolidity/lsp/FileRepository.h> #include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/lsp/Utils.h> #include <libsolidity/lsp/Utils.h>
#include <fmt/format.h> #include <regex>
#include <fstream> #include <fstream>
namespace solidity::lsp namespace solidity::lsp
@ -115,4 +115,15 @@ optional<SourceLocation> parseRange(FileRepository const& _fileRepository, strin
return start; 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;
}
} }

View File

@ -64,6 +64,13 @@ std::optional<langutil::SourceLocation> parseRange(
Json::Value const& _range 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. /// Extracts the resolved declaration of the given expression AST node.
/// ///
/// This may for example be the type declaration of an identifier, /// This may for example be the type declaration of an identifier,

View File

@ -912,7 +912,7 @@ void CommandLineInterface::handleAst()
void CommandLineInterface::serveLSP() void CommandLineInterface::serveLSP()
{ {
lsp::IOStreamTransport transport; lsp::StdioTransport transport;
if (!lsp::LanguageServer{transport}.run()) if (!lsp::LanguageServer{transport}.run())
solThrow(CommandLineExecutionError, "LSP terminated abnormally."); solThrow(CommandLineExecutionError, "LSP terminated abnormally.");
} }

View File

@ -1,25 +1,57 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# pragma pylint: disable=too-many-lines # pragma pylint: disable=too-many-lines
# test line 1
import argparse import argparse
import fnmatch import fnmatch
import functools
import json import json
import os import os
import re
import subprocess import subprocess
import sys import sys
import traceback import traceback
import re
import tty
import functools
from collections import namedtuple from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from typing import Any, List, Optional, Tuple, Union
from itertools import islice
from enum import Enum, auto 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 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. Named tuple that holds various regexes used to parse the test specification.
""" """
@ -100,7 +132,6 @@ class BadHeader(Exception):
def __init__(self, msg: str): def __init__(self, msg: str):
super().__init__("Bad header: " + msg) super().__init__("Bad header: " + msg)
class JsonRpcProcess: class JsonRpcProcess:
exe_path: str exe_path: str
exe_args: List[str] exe_args: List[str]
@ -143,10 +174,12 @@ class JsonRpcProcess:
# server quit # server quit
return None return None
line = line.decode("utf-8") line = line.decode("utf-8")
if self.trace_io:
print(f"Received header-line: {escape_string(line)}")
if not line.endswith("\r\n"): if not line.endswith("\r\n"):
raise BadHeader("missing newline") raise BadHeader("missing newline")
# remove the "\r\n" # Safely remove the "\r\n".
line = line[:-2] line = line.rstrip("\r\n")
if line == '': if line == '':
break # done with the headers break # done with the headers
if line.startswith(CONTENT_LENGTH_HEADER): if line.startswith(CONTENT_LENGTH_HEADER):
@ -588,7 +621,7 @@ class FileTestRunner:
while True: while True:
print("(u)pdate/(r)etry/(i)gnore?") print("(u)pdate/(r)etry/(i)gnore?")
user_response = sys.stdin.read(1) user_response = getCharFromStdin()
if user_response == "i": if user_response == "i":
return self.TestResult.SuccessOrIgnored return self.TestResult.SuccessOrIgnored
@ -694,7 +727,7 @@ class SolidityLSPTestSuite: # {{{
args = create_cli_parser().parse_args() args = create_cli_parser().parse_args()
self.solc_path = args.solc_path self.solc_path = args.solc_path
self.project_root_dir = os.path.realpath(args.project_root_dir) + "/test/libsolidity/lsp" 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.print_assertions = args.print_assertions
self.trace_io = args.trace_io self.trace_io = args.trace_io
self.test_pattern = args.test_pattern self.test_pattern = args.test_pattern
@ -777,7 +810,7 @@ class SolidityLSPTestSuite: # {{{
return f"{self.project_root_dir}/{test_case_name}.sol" return f"{self.project_root_dir}/{test_case_name}.sol"
def get_test_file_uri(self, test_case_name): 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): def get_test_file_contents(self, test_case_name):
""" """
@ -786,7 +819,7 @@ class SolidityLSPTestSuite: # {{{
in the test path (test/libsolidity/lsp). 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: 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: def require_params_for_method(self, method_name: str, message: dict) -> Any:
""" """
@ -1058,7 +1091,7 @@ class SolidityLSPTestSuite: # {{{
""" """
while True: while True:
print("(u)pdate/(r)etry/(s)kip file?") print("(u)pdate/(r)etry/(s)kip file?")
user_response = sys.stdin.read(1) user_response = getCharFromStdin()
if user_response == "u": if user_response == "u":
while True: while True:
try: try:
@ -1067,8 +1100,8 @@ class SolidityLSPTestSuite: # {{{
# pragma pylint: disable=broad-except # pragma pylint: disable=broad-except
except Exception as e: except Exception as e:
print(e) print(e)
if ret := self.user_interaction_failed_autoupdate(test): if self.user_interaction_failed_autoupdate(test):
return ret return True
elif user_response == 's': elif user_response == 's':
return True return True
elif user_response == 'r': elif user_response == 'r':
@ -1076,7 +1109,7 @@ class SolidityLSPTestSuite: # {{{
def user_interaction_failed_autoupdate(self, test): def user_interaction_failed_autoupdate(self, test):
print("(e)dit/(r)etry/(s)kip file?") print("(e)dit/(r)etry/(s)kip file?")
user_response = sys.stdin.read(1) user_response = getCharFromStdin()
if user_response == "r": if user_response == "r":
print("retrying...") print("retrying...")
# pragma pylint: disable=no-member # pragma pylint: disable=no-member
@ -1141,7 +1174,7 @@ class SolidityLSPTestSuite: # {{{
marker = self.get_file_tags("lib")["@diagnostics"] marker = self.get_file_tags("lib")["@diagnostics"]
self.expect_diagnostic(report['diagnostics'][0], code=2072, marker=marker) 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): 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 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) 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', { solc.send_message('textDocument/didOpen', {
'textDocument': { 'textDocument': {
'uri': FILE_A_URI, 'uri': FILE_A_URI,
@ -1466,7 +1499,7 @@ class SolidityLSPTestSuite: # {{{
) )
reports = self.wait_for_diagnostics(solc) reports = self.wait_for_diagnostics(solc)
self.expect_equal(len(reports), 1, '') 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") self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics")
def test_textDocument_didChange_at_eol(self, solc: JsonRpcProcess) -> None: def test_textDocument_didChange_at_eol(self, solc: JsonRpcProcess) -> None:
@ -1652,10 +1685,8 @@ class SolidityLSPTestSuite: # {{{
# }}} # }}}
# }}} # }}}
if __name__ == "__main__": 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() suite = SolidityLSPTestSuite()
exit_code = suite.main() exit_code = suite.main()
sys.exit(exit_code) sys.exit(exit_code)