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

View File

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

View File

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

View File

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

View File

@ -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;
};
}

View File

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

View File

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

View File

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

View File

@ -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);
}
// }}}

View File

@ -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;
};
}

View File

@ -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;
}
}

View File

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

View File

@ -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.");
}

View File

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