mirror of
https://github.com/ethereum/solidity
synced 2023-10-03 13:03:40 +00:00
Merge pull request #13150 from ethereum/lsp-analyze-all-in-project
LSP analyze all files in project
This commit is contained in:
commit
44fcf351ba
@ -9,6 +9,7 @@ Language Features:
|
|||||||
Compiler Features:
|
Compiler Features:
|
||||||
* Code Generator: More efficient overflow checks for multiplication.
|
* Code Generator: More efficient overflow checks for multiplication.
|
||||||
* Yul Optimizer: Simplify the starting offset of zero-length operations to zero.
|
* Yul Optimizer: Simplify the starting offset of zero-length operations to zero.
|
||||||
|
* Language Server: Analyze all files in a project by default (can be customized by setting ``'file-load-strategy'`` to ``'directly-opened-and-on-import'`` in LSP settings object).
|
||||||
|
|
||||||
|
|
||||||
Bugfixes:
|
Bugfixes:
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
// 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/Transport.h>
|
||||||
#include <libsolidity/lsp/Utils.h>
|
#include <libsolidity/lsp/Utils.h>
|
||||||
|
|
||||||
#include <libsolutil/StringUtils.h>
|
#include <libsolutil/StringUtils.h>
|
||||||
@ -25,11 +26,14 @@
|
|||||||
#include <range/v3/algorithm/none_of.hpp>
|
#include <range/v3/algorithm/none_of.hpp>
|
||||||
#include <range/v3/range/conversion.hpp>
|
#include <range/v3/range/conversion.hpp>
|
||||||
#include <range/v3/view/transform.hpp>
|
#include <range/v3/view/transform.hpp>
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace solidity;
|
using namespace solidity;
|
||||||
using namespace solidity::lsp;
|
using namespace solidity::lsp;
|
||||||
@ -84,6 +88,7 @@ string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const
|
|||||||
|
|
||||||
string FileRepository::uriToSourceUnitName(string const& _path) const
|
string FileRepository::uriToSourceUnitName(string const& _path) const
|
||||||
{
|
{
|
||||||
|
lspAssert(boost::algorithm::starts_with(_path, "file://"), ErrorCode::InternalError, "URI must start with file://");
|
||||||
return stripFileUriSchemePrefix(_path);
|
return stripFileUriSchemePrefix(_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +97,7 @@ void FileRepository::setSourceByUri(string const& _uri, string _source)
|
|||||||
// 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.
|
||||||
auto sourceUnitName = uriToSourceUnitName(_uri);
|
auto sourceUnitName = uriToSourceUnitName(_uri);
|
||||||
|
lspDebug(fmt::format("FileRepository.setSourceByUri({}): {}", _uri, _source));
|
||||||
m_sourceUnitNamesToUri.emplace(sourceUnitName, _uri);
|
m_sourceUnitNamesToUri.emplace(sourceUnitName, _uri);
|
||||||
m_sourceCodes[sourceUnitName] = std::move(_source);
|
m_sourceCodes[sourceUnitName] = std::move(_source);
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
#include <liblangutil/SourceReferenceExtractor.h>
|
#include <liblangutil/SourceReferenceExtractor.h>
|
||||||
#include <liblangutil/CharStream.h>
|
#include <liblangutil/CharStream.h>
|
||||||
|
|
||||||
|
#include <libsolutil/CommonIO.h>
|
||||||
#include <libsolutil/Visitor.h>
|
#include <libsolutil/Visitor.h>
|
||||||
#include <libsolutil/JSON.h>
|
#include <libsolutil/JSON.h>
|
||||||
|
|
||||||
@ -42,6 +43,8 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
|
|
||||||
using namespace std;
|
using namespace std;
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
using namespace std::placeholders;
|
using namespace std::placeholders;
|
||||||
@ -50,9 +53,25 @@ using namespace solidity::lsp;
|
|||||||
using namespace solidity::langutil;
|
using namespace solidity::langutil;
|
||||||
using namespace solidity::frontend;
|
using namespace solidity::frontend;
|
||||||
|
|
||||||
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
|
||||||
|
bool resolvesToRegularFile(boost::filesystem::path _path, int maxRecursionDepth = 10)
|
||||||
|
{
|
||||||
|
fs::file_status fileStatus = fs::status(_path);
|
||||||
|
|
||||||
|
while (fileStatus.type() == fs::file_type::symlink_file && maxRecursionDepth > 0)
|
||||||
|
{
|
||||||
|
_path = boost::filesystem::read_symlink(_path);
|
||||||
|
fileStatus = fs::status(_path);
|
||||||
|
maxRecursionDepth--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileStatus.type() == fs::file_type::regular_file;
|
||||||
|
}
|
||||||
|
|
||||||
int toDiagnosticSeverity(Error::Type _errorType)
|
int toDiagnosticSeverity(Error::Type _errorType)
|
||||||
{
|
{
|
||||||
// 1=Error, 2=Warning, 3=Info, 4=Hint
|
// 1=Error, 2=Warning, 3=Info, 4=Hint
|
||||||
@ -118,7 +137,7 @@ LanguageServer::LanguageServer(Transport& _transport):
|
|||||||
{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}},
|
{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}},
|
||||||
{"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }},
|
{"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }},
|
||||||
{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)},
|
{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)},
|
||||||
{"initialized", [](auto, auto) {}},
|
{"initialized", bind(&LanguageServer::handleInitialized, this, _1, _2)},
|
||||||
{"$/setTrace", [this](auto, Json::Value const& args) { setTrace(args["value"]); }},
|
{"$/setTrace", [this](auto, Json::Value const& args) { setTrace(args["value"]); }},
|
||||||
{"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }},
|
{"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }},
|
||||||
{"textDocument/definition", GotoDefinition(*this) },
|
{"textDocument/definition", GotoDefinition(*this) },
|
||||||
@ -147,6 +166,26 @@ Json::Value LanguageServer::toJson(SourceLocation const& _location)
|
|||||||
|
|
||||||
void LanguageServer::changeConfiguration(Json::Value const& _settings)
|
void LanguageServer::changeConfiguration(Json::Value const& _settings)
|
||||||
{
|
{
|
||||||
|
// The settings item: "file-load-strategy" (enum) defaults to "project-directory" if not (or not correctly) set.
|
||||||
|
// It can be overridden during client's handshake or at runtime, as usual.
|
||||||
|
//
|
||||||
|
// If this value is set to "project-directory" (default), all .sol files located inside the project directory or reachable through symbolic links will be subject to operations.
|
||||||
|
//
|
||||||
|
// Operations include compiler analysis, but also finding all symbolic references or symbolic renaming.
|
||||||
|
//
|
||||||
|
// If this value is set to "directly-opened-and-on-import", then only currently directly opened files and
|
||||||
|
// those files being imported directly or indirectly will be included in operations.
|
||||||
|
if (_settings["file-load-strategy"])
|
||||||
|
{
|
||||||
|
auto const text = _settings["file-load-strategy"].asString();
|
||||||
|
if (text == "project-directory")
|
||||||
|
m_fileLoadStrategy = FileLoadStrategy::ProjectDirectory;
|
||||||
|
else if (text == "directly-opened-and-on-import")
|
||||||
|
m_fileLoadStrategy = FileLoadStrategy::DirectlyOpenedAndOnImported;
|
||||||
|
else
|
||||||
|
lspAssert(false, ErrorCode::InvalidParams, "Invalid file load strategy: " + text);
|
||||||
|
}
|
||||||
|
|
||||||
m_settingsObject = _settings;
|
m_settingsObject = _settings;
|
||||||
Json::Value jsonIncludePaths = _settings["include-paths"];
|
Json::Value jsonIncludePaths = _settings["include-paths"];
|
||||||
|
|
||||||
@ -173,6 +212,24 @@ void LanguageServer::changeConfiguration(Json::Value const& _settings)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vector<boost::filesystem::path> LanguageServer::allSolidityFilesFromProject() const
|
||||||
|
{
|
||||||
|
vector<fs::path> collectedPaths{};
|
||||||
|
|
||||||
|
// We explicitly decided against including all files from include paths but leave the possibility
|
||||||
|
// open for a future PR to enable such a feature to be optionally enabled (default disabled).
|
||||||
|
|
||||||
|
auto directoryIterator = fs::recursive_directory_iterator(m_fileRepository.basePath(), fs::symlink_option::recurse);
|
||||||
|
for (fs::directory_entry const& dirEntry: directoryIterator)
|
||||||
|
if (
|
||||||
|
dirEntry.path().extension() == ".sol" &&
|
||||||
|
(dirEntry.status().type() == fs::file_type::regular_file || resolvesToRegularFile(dirEntry.path()))
|
||||||
|
)
|
||||||
|
collectedPaths.push_back(dirEntry.path());
|
||||||
|
|
||||||
|
return collectedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
void LanguageServer::compile()
|
void LanguageServer::compile()
|
||||||
{
|
{
|
||||||
// For files that are not open, we have to take changes on disk into account,
|
// For files that are not open, we have to take changes on disk into account,
|
||||||
@ -181,6 +238,18 @@ void LanguageServer::compile()
|
|||||||
FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths());
|
FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths());
|
||||||
swap(oldRepository, m_fileRepository);
|
swap(oldRepository, m_fileRepository);
|
||||||
|
|
||||||
|
// Load all solidity files from project.
|
||||||
|
if (m_fileLoadStrategy == FileLoadStrategy::ProjectDirectory)
|
||||||
|
for (auto const& projectFile: allSolidityFilesFromProject())
|
||||||
|
{
|
||||||
|
lspDebug(fmt::format("adding project file: {}", projectFile.generic_string()));
|
||||||
|
m_fileRepository.setSourceByUri(
|
||||||
|
m_fileRepository.sourceUnitNameToUri(projectFile.generic_string()),
|
||||||
|
util::readFileAsString(projectFile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite all files as opened by the client, including the ones which might potentially have changes.
|
||||||
for (string const& fileName: m_openFiles)
|
for (string const& fileName: m_openFiles)
|
||||||
m_fileRepository.setSourceByUri(
|
m_fileRepository.setSourceByUri(
|
||||||
fileName,
|
fileName,
|
||||||
@ -269,6 +338,7 @@ bool LanguageServer::run()
|
|||||||
{
|
{
|
||||||
string const methodName = (*jsonMessage)["method"].asString();
|
string const methodName = (*jsonMessage)["method"].asString();
|
||||||
id = (*jsonMessage)["id"];
|
id = (*jsonMessage)["id"];
|
||||||
|
lspDebug(fmt::format("received method call: {}", methodName));
|
||||||
|
|
||||||
if (auto handler = util::valueOrDefault(m_handlers, methodName))
|
if (auto handler = util::valueOrDefault(m_handlers, methodName))
|
||||||
handler(id, (*jsonMessage)["params"]);
|
handler(id, (*jsonMessage)["params"]);
|
||||||
@ -278,6 +348,10 @@ bool LanguageServer::run()
|
|||||||
else
|
else
|
||||||
m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string.");
|
m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string.");
|
||||||
}
|
}
|
||||||
|
catch (Json::Exception const&)
|
||||||
|
{
|
||||||
|
m_client.error(id, ErrorCode::InvalidParams, "JSON object access error. Most likely due to a badly formatted JSON request message."s);
|
||||||
|
}
|
||||||
catch (RequestError const& error)
|
catch (RequestError const& error)
|
||||||
{
|
{
|
||||||
m_client.error(id, error.code(), error.comment() ? *error.comment() : ""s);
|
m_client.error(id, error.code(), error.comment() ? *error.comment() : ""s);
|
||||||
@ -347,6 +421,12 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
|
|||||||
m_client.reply(_id, move(replyArgs));
|
m_client.reply(_id, move(replyArgs));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LanguageServer::handleInitialized(MessageID, Json::Value const&)
|
||||||
|
{
|
||||||
|
if (m_fileLoadStrategy == FileLoadStrategy::ProjectDirectory)
|
||||||
|
compileAndUpdateDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args)
|
void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args)
|
||||||
{
|
{
|
||||||
auto uri = _args["textDocument"]["uri"];
|
auto uri = _args["textDocument"]["uri"];
|
||||||
|
@ -36,6 +36,23 @@ namespace solidity::lsp
|
|||||||
class RenameSymbol;
|
class RenameSymbol;
|
||||||
enum class ErrorCode;
|
enum class ErrorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum to mandate what files to take into consideration for source code analysis.
|
||||||
|
*/
|
||||||
|
enum class FileLoadStrategy
|
||||||
|
{
|
||||||
|
/// Takes only those files into consideration that are explicitly opened and those
|
||||||
|
/// that have been directly or indirectly imported.
|
||||||
|
DirectlyOpenedAndOnImported = 0,
|
||||||
|
|
||||||
|
/// Takes all Solidity (.sol) files within the project root into account.
|
||||||
|
/// Symbolic links will be followed, even if they lead outside of the project directory
|
||||||
|
/// (`--allowed-paths` is currently ignored by the LSP).
|
||||||
|
///
|
||||||
|
/// This resembles the closest what other LSPs should be doing already.
|
||||||
|
ProjectDirectory = 1,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Solidity Language Server, managing one LSP client.
|
* Solidity Language Server, managing one LSP client.
|
||||||
* This implements a subset of LSP version 3.16 that can be found at:
|
* This implements a subset of LSP version 3.16 that can be found at:
|
||||||
@ -68,6 +85,7 @@ private:
|
|||||||
/// Reports an error and returns false if not.
|
/// Reports an error and returns false if not.
|
||||||
void requireServerInitialized();
|
void requireServerInitialized();
|
||||||
void handleInitialize(MessageID _id, Json::Value const& _args);
|
void handleInitialize(MessageID _id, Json::Value const& _args);
|
||||||
|
void handleInitialized(MessageID _id, Json::Value const& _args);
|
||||||
void handleWorkspaceDidChangeConfiguration(Json::Value const& _args);
|
void handleWorkspaceDidChangeConfiguration(Json::Value const& _args);
|
||||||
void setTrace(Json::Value const& _args);
|
void setTrace(Json::Value const& _args);
|
||||||
void handleTextDocumentDidOpen(Json::Value const& _args);
|
void handleTextDocumentDidOpen(Json::Value const& _args);
|
||||||
@ -82,6 +100,9 @@ private:
|
|||||||
|
|
||||||
/// Compile everything until after analysis phase.
|
/// Compile everything until after analysis phase.
|
||||||
void compile();
|
void compile();
|
||||||
|
|
||||||
|
std::vector<boost::filesystem::path> allSolidityFilesFromProject() const;
|
||||||
|
|
||||||
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
|
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
|
||||||
|
|
||||||
Json::Value toRange(langutil::SourceLocation const& _location);
|
Json::Value toRange(langutil::SourceLocation const& _location);
|
||||||
@ -100,6 +121,7 @@ private:
|
|||||||
/// 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;
|
||||||
FileRepository m_fileRepository;
|
FileRepository m_fileRepository;
|
||||||
|
FileLoadStrategy m_fileLoadStrategy = FileLoadStrategy::ProjectDirectory;
|
||||||
|
|
||||||
frontend::CompilerStack m_compilerStack;
|
frontend::CompilerStack m_compilerStack;
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
*/
|
*/
|
||||||
// SPDX-License-Identifier: GPL-3.0
|
// SPDX-License-Identifier: GPL-3.0
|
||||||
#include <libsolidity/lsp/Transport.h>
|
#include <libsolidity/lsp/Transport.h>
|
||||||
|
#include <libsolidity/lsp/Utils.h>
|
||||||
|
|
||||||
#include <libsolutil/JSON.h>
|
#include <libsolutil/JSON.h>
|
||||||
#include <libsolutil/Visitor.h>
|
#include <libsolutil/Visitor.h>
|
||||||
@ -205,11 +206,13 @@ std::string StdioTransport::getline()
|
|||||||
{
|
{
|
||||||
std::string line;
|
std::string line;
|
||||||
std::getline(std::cin, line);
|
std::getline(std::cin, line);
|
||||||
|
lspDebug(fmt::format("Received: {}", line));
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
void StdioTransport::writeBytes(std::string_view _data)
|
void StdioTransport::writeBytes(std::string_view _data)
|
||||||
{
|
{
|
||||||
|
lspDebug(fmt::format("Sending: {}", _data));
|
||||||
auto const bytesWritten = fwrite(_data.data(), 1, _data.size(), stdout);
|
auto const bytesWritten = fwrite(_data.data(), 1, _data.size(), stdout);
|
||||||
solAssert(bytesWritten == _data.size());
|
solAssert(bytesWritten == _data.size());
|
||||||
}
|
}
|
||||||
|
6
test/libsolidity/lsp/analyze-full-project/C.sol
Normal file
6
test/libsolidity/lsp/analyze-full-project/C.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract C
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/analyze-full-project/D.sol
Normal file
6
test/libsolidity/lsp/analyze-full-project/D.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract D
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/analyze-full-project/E.sol
Normal file
6
test/libsolidity/lsp/analyze-full-project/E.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract E
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
import "otherlib/second.sol";
|
||||||
|
|
||||||
|
contract C
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested-2/A/B/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested-2/A/B/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract B
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested-2/A/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested-2/A/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract A
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested-2/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested-2/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract RootContract
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested/A/B/C/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested/A/B/C/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract C
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested/A/B/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested/A/B/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract B
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested/A/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested/A/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract A
|
||||||
|
{
|
||||||
|
}
|
6
test/libsolidity/lsp/include-paths-nested/foo.sol
Normal file
6
test/libsolidity/lsp/include-paths-nested/foo.sol
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
contract RootContract
|
||||||
|
{
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
// SPDX-License-Identifier: UNLICENSED
|
||||||
|
pragma solidity >=0.8.0;
|
||||||
|
|
||||||
|
library Second
|
||||||
|
{
|
||||||
|
function f(uint n) public pure returns (uint) { return n + 1; }
|
||||||
|
}
|
173
test/lsp.py
173
test/lsp.py
@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# pragma pylint: disable=too-many-lines
|
# pragma pylint: disable=too-many-lines
|
||||||
# test line 1
|
# test line 1
|
||||||
|
from __future__ import annotations # See: https://github.com/PyCQA/pylint/issues/3320
|
||||||
import argparse
|
import argparse
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
@ -422,7 +423,7 @@ class TestParser:
|
|||||||
self.next_line()
|
self.next_line()
|
||||||
|
|
||||||
|
|
||||||
def parseDiagnostics(self) -> Diagnostics:
|
def parseDiagnostics(self) -> TestParser.Diagnostics:
|
||||||
"""
|
"""
|
||||||
Parse diagnostic expectations specified in the file.
|
Parse diagnostic expectations specified in the file.
|
||||||
Returns a named tuple instance of "Diagnostics"
|
Returns a named tuple instance of "Diagnostics"
|
||||||
@ -454,7 +455,7 @@ class TestParser:
|
|||||||
return self.Diagnostics(**diagnostics)
|
return self.Diagnostics(**diagnostics)
|
||||||
|
|
||||||
|
|
||||||
def parseRequestAndResponse(self) -> RequestAndResponse:
|
def parseRequestAndResponse(self) -> TestParser.RequestAndResponse:
|
||||||
RESPONSE_START = "// <- "
|
RESPONSE_START = "// <- "
|
||||||
REQUEST_END = "// }"
|
REQUEST_END = "// }"
|
||||||
COMMENT_PREFIX = "// "
|
COMMENT_PREFIX = "// "
|
||||||
@ -537,6 +538,11 @@ class TestParser:
|
|||||||
"""
|
"""
|
||||||
return self.current_line_tuple is None
|
return self.current_line_tuple is None
|
||||||
|
|
||||||
|
class FileLoadStrategy(Enum):
|
||||||
|
Undefined = None
|
||||||
|
ProjectDirectory = 'project-directory'
|
||||||
|
DirectlyOpenedAndOnImport = 'directly-opened-and-on-import'
|
||||||
|
|
||||||
class FileTestRunner:
|
class FileTestRunner:
|
||||||
"""
|
"""
|
||||||
Runs all tests in a given file.
|
Runs all tests in a given file.
|
||||||
@ -898,18 +904,27 @@ class SolidityLSPTestSuite: # {{{
|
|||||||
|
|
||||||
return min(max(self.test_counter.failed, self.assertion_counter.failed), 127)
|
return min(max(self.test_counter.failed, self.assertion_counter.failed), 127)
|
||||||
|
|
||||||
def setup_lsp(self, lsp: JsonRpcProcess, expose_project_root=True):
|
def setup_lsp(
|
||||||
|
self,
|
||||||
|
lsp: JsonRpcProcess,
|
||||||
|
expose_project_root=True,
|
||||||
|
file_load_strategy: FileLoadStrategy=FileLoadStrategy.DirectlyOpenedAndOnImport,
|
||||||
|
custom_include_paths: list[str] = None,
|
||||||
|
project_root_subdir=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Prepares the solc LSP server by calling `initialize`,
|
Prepares the solc LSP server by calling `initialize`,
|
||||||
and `initialized` methods.
|
and `initialized` methods.
|
||||||
"""
|
"""
|
||||||
|
project_root_uri_with_maybe_subdir = self.project_root_uri
|
||||||
|
if project_root_subdir is not None:
|
||||||
|
project_root_uri_with_maybe_subdir = self.project_root_uri + '/' + project_root_subdir
|
||||||
params = {
|
params = {
|
||||||
'processId': None,
|
'processId': None,
|
||||||
'rootUri': self.project_root_uri,
|
'rootUri': project_root_uri_with_maybe_subdir,
|
||||||
# Enable traces to receive the amount of expected diagnostics before
|
# Enable traces to receive the amount of expected diagnostics before
|
||||||
# actually receiving them.
|
# actually receiving them.
|
||||||
'trace': 'messages',
|
'trace': 'messages',
|
||||||
'initializationOptions': {},
|
|
||||||
'capabilities': {
|
'capabilities': {
|
||||||
'textDocument': {
|
'textDocument': {
|
||||||
'publishDiagnostics': {'relatedInformation': True}
|
'publishDiagnostics': {'relatedInformation': True}
|
||||||
@ -923,8 +938,19 @@ class SolidityLSPTestSuite: # {{{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if file_load_strategy != FileLoadStrategy.Undefined:
|
||||||
|
params['initializationOptions'] = {}
|
||||||
|
params['initializationOptions']['file-load-strategy'] = file_load_strategy.value
|
||||||
|
|
||||||
|
if custom_include_paths is not None and len(custom_include_paths) != 0:
|
||||||
|
if params['initializationOptions'] is None:
|
||||||
|
params['initializationOptions'] = {}
|
||||||
|
params['initializationOptions']['include-paths'] = custom_include_paths
|
||||||
|
|
||||||
if not expose_project_root:
|
if not expose_project_root:
|
||||||
params['rootUri'] = None
|
params['rootUri'] = None
|
||||||
|
|
||||||
lsp.call_method('initialize', params)
|
lsp.call_method('initialize', params)
|
||||||
lsp.send_notification('initialized')
|
lsp.send_notification('initialized')
|
||||||
|
|
||||||
@ -1059,6 +1085,14 @@ class SolidityLSPTestSuite: # {{{
|
|||||||
)
|
)
|
||||||
return self.wait_for_diagnostics(solc_process)
|
return self.wait_for_diagnostics(solc_process)
|
||||||
|
|
||||||
|
def expect_true(
|
||||||
|
self,
|
||||||
|
actual,
|
||||||
|
description="Expected True value",
|
||||||
|
part=ExpectationFailed.Part.Diagnostics
|
||||||
|
) -> None:
|
||||||
|
self.expect_equal(actual, True, description, part)
|
||||||
|
|
||||||
def expect_equal(
|
def expect_equal(
|
||||||
self,
|
self,
|
||||||
actual,
|
actual,
|
||||||
@ -1295,6 +1329,98 @@ class SolidityLSPTestSuite: # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# {{{ actual tests
|
# {{{ actual tests
|
||||||
|
def test_analyze_all_project_files_flat(self, solc: JsonRpcProcess) -> None:
|
||||||
|
"""
|
||||||
|
Tests the option (default) to analyze all .sol project files even when they have not been actively
|
||||||
|
opened yet. This is how other LSPs (at least for C++) work too and it makes cross-unit tasks
|
||||||
|
actually correct (e.g. symbolic rename, find all references, ...).
|
||||||
|
|
||||||
|
In this test, we simply open up a custom project and ensure we're receiving the diagnostics
|
||||||
|
for all existing files in that project (while having none of these files opened).
|
||||||
|
"""
|
||||||
|
SUBDIR = 'analyze-full-project'
|
||||||
|
self.setup_lsp(
|
||||||
|
solc,
|
||||||
|
file_load_strategy=FileLoadStrategy.ProjectDirectory,
|
||||||
|
project_root_subdir=SUBDIR
|
||||||
|
)
|
||||||
|
published_diagnostics = self.wait_for_diagnostics(solc)
|
||||||
|
self.expect_equal(len(published_diagnostics), 3, "Diagnostic reports for 3 files")
|
||||||
|
|
||||||
|
# C.sol
|
||||||
|
report = published_diagnostics[0]
|
||||||
|
self.expect_equal(report['uri'], self.get_test_file_uri('C', SUBDIR), "Correct file URI")
|
||||||
|
self.expect_equal(len(report['diagnostics']), 0, "no diagnostics")
|
||||||
|
|
||||||
|
# D.sol
|
||||||
|
report = published_diagnostics[1]
|
||||||
|
self.expect_equal(report['uri'], self.get_test_file_uri('D', SUBDIR), "Correct file URI")
|
||||||
|
self.expect_equal(len(report['diagnostics']), 0, "no diagnostics")
|
||||||
|
|
||||||
|
# E.sol
|
||||||
|
report = published_diagnostics[2]
|
||||||
|
self.expect_equal(report['uri'], self.get_test_file_uri('E', SUBDIR), "Correct file URI")
|
||||||
|
self.expect_equal(len(report['diagnostics']), 0, "no diagnostics")
|
||||||
|
|
||||||
|
def test_analyze_all_project_files_nested(self, solc: JsonRpcProcess) -> None:
|
||||||
|
"""
|
||||||
|
Same as first test on that matter but with deeper nesting levels.
|
||||||
|
"""
|
||||||
|
SUBDIR = 'include-paths-nested'
|
||||||
|
EXPECTED_FILES = {
|
||||||
|
"A/B/C/foo",
|
||||||
|
"A/B/foo",
|
||||||
|
"A/foo",
|
||||||
|
"foo",
|
||||||
|
}
|
||||||
|
EXPECTED_URIS = {self.get_test_file_uri(x, SUBDIR) for x in EXPECTED_FILES}
|
||||||
|
self.setup_lsp(
|
||||||
|
solc,
|
||||||
|
file_load_strategy=FileLoadStrategy.ProjectDirectory,
|
||||||
|
project_root_subdir=SUBDIR
|
||||||
|
)
|
||||||
|
published_diagnostics = self.wait_for_diagnostics(solc)
|
||||||
|
self.expect_equal(len(published_diagnostics), len(EXPECTED_FILES), "Test number of files analyzed.")
|
||||||
|
self.expect_equal({report['uri'] for report in published_diagnostics}, EXPECTED_URIS)
|
||||||
|
self.expect_equal([len(report['diagnostics']) for report in published_diagnostics], [0] * len(EXPECTED_URIS))
|
||||||
|
|
||||||
|
def test_analyze_all_project_files_nested_with_include_paths(self, solc: JsonRpcProcess) -> None:
|
||||||
|
"""
|
||||||
|
Same as first test on that matter but with deeper nesting levels.
|
||||||
|
"""
|
||||||
|
SUBDIR = 'include-paths-nested-2'
|
||||||
|
EXPECTED_FILES = {
|
||||||
|
"A/B/C/foo",
|
||||||
|
"A/B/foo",
|
||||||
|
"A/foo",
|
||||||
|
"foo",
|
||||||
|
}
|
||||||
|
IMPLICITLY_LOADED_FILE_COUNT = 1
|
||||||
|
EXPECTED_URIS = {self.get_test_file_uri(x, SUBDIR) for x in EXPECTED_FILES}
|
||||||
|
self.setup_lsp(
|
||||||
|
solc,
|
||||||
|
file_load_strategy=FileLoadStrategy.ProjectDirectory,
|
||||||
|
project_root_subdir=SUBDIR,
|
||||||
|
custom_include_paths=[f"{self.project_root_dir}/other-include-dir"]
|
||||||
|
)
|
||||||
|
published_diagnostics = self.wait_for_diagnostics(solc)
|
||||||
|
self.expect_equal(
|
||||||
|
len(published_diagnostics),
|
||||||
|
len(EXPECTED_FILES) + IMPLICITLY_LOADED_FILE_COUNT,
|
||||||
|
"Test number of files analyzed."
|
||||||
|
)
|
||||||
|
|
||||||
|
# All but the last report should be from expected files
|
||||||
|
for report in published_diagnostics[:-IMPLICITLY_LOADED_FILE_COUNT]:
|
||||||
|
self.expect_true(report['uri'] in EXPECTED_URIS, "Correct file URI")
|
||||||
|
self.expect_equal(len(report['diagnostics']), 0, "no diagnostics")
|
||||||
|
|
||||||
|
# Check last report (should be the custom imported lib).
|
||||||
|
# This file is analyzed because it was imported via "A/B/C/foo.sol".
|
||||||
|
last_report = published_diagnostics[len(EXPECTED_URIS)]
|
||||||
|
self.expect_equal(last_report['uri'], self.get_test_file_uri('second', 'other-include-dir/otherlib'), "Correct file URI")
|
||||||
|
self.expect_equal(len(last_report['diagnostics']), 0, "no diagnostics")
|
||||||
|
|
||||||
|
|
||||||
def test_publish_diagnostics_errors_multiline(self, solc: JsonRpcProcess) -> None:
|
def test_publish_diagnostics_errors_multiline(self, solc: JsonRpcProcess) -> None:
|
||||||
self.setup_lsp(solc)
|
self.setup_lsp(solc)
|
||||||
@ -1408,6 +1534,43 @@ class SolidityLSPTestSuite: # {{{
|
|||||||
self.expect_equal(len(diagnostics), 1, "no diagnostics")
|
self.expect_equal(len(diagnostics), 1, "no diagnostics")
|
||||||
self.expect_diagnostic(diagnostics[0], code=2018, lineNo=5, startEndColumns=(4, 62))
|
self.expect_diagnostic(diagnostics[0], code=2018, lineNo=5, startEndColumns=(4, 62))
|
||||||
|
|
||||||
|
def test_custom_includes_with_full_project(self, solc: JsonRpcProcess) -> None:
|
||||||
|
"""
|
||||||
|
Tests loading all project files while having custom include directories configured.
|
||||||
|
In such a scenario, all project files should be analyzed and those being included via search path
|
||||||
|
but not those include files that are not directly nor indirectly included.
|
||||||
|
"""
|
||||||
|
self.setup_lsp(
|
||||||
|
solc,
|
||||||
|
expose_project_root=True,
|
||||||
|
project_root_subdir=''
|
||||||
|
)
|
||||||
|
solc.send_notification(
|
||||||
|
'workspace/didChangeConfiguration', {
|
||||||
|
'settings': {
|
||||||
|
'include-paths': [
|
||||||
|
f"{self.project_root_dir}/other-include-dir"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, 'include-paths/using-custom-includes')
|
||||||
|
|
||||||
|
self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files")
|
||||||
|
|
||||||
|
# test file
|
||||||
|
report = published_diagnostics[0]
|
||||||
|
self.expect_equal(report['uri'], self.get_test_file_uri('using-custom-includes', 'include-paths'))
|
||||||
|
diagnostics = report['diagnostics']
|
||||||
|
self.expect_equal(len(diagnostics), 0, "no diagnostics")
|
||||||
|
|
||||||
|
# imported file
|
||||||
|
report = published_diagnostics[1]
|
||||||
|
self.expect_equal(report['uri'], f"{self.project_root_uri}/other-include-dir/otherlib/otherlib.sol")
|
||||||
|
diagnostics = report['diagnostics']
|
||||||
|
self.expect_equal(len(diagnostics), 1)
|
||||||
|
self.expect_diagnostic(diagnostics[0], code=2018, lineNo=5, startEndColumns=(4, 62))
|
||||||
|
|
||||||
def test_didChange_in_A_causing_error_in_B(self, solc: JsonRpcProcess) -> None:
|
def test_didChange_in_A_causing_error_in_B(self, solc: JsonRpcProcess) -> None:
|
||||||
# Reusing another test but now change some file that generates an error in the other.
|
# Reusing another test but now change some file that generates an error in the other.
|
||||||
self.test_textDocument_didOpen_with_relative_import(solc)
|
self.test_textDocument_didOpen_with_relative_import(solc)
|
||||||
|
Loading…
Reference in New Issue
Block a user