Skip to content

Commit 44fcf35

Browse files
Merge pull request #13150 from ethereum/lsp-analyze-all-in-project
LSP analyze all files in project
2 parents a3de6cd + c8074d2 commit 44fcf35

File tree

18 files changed

+356
-6
lines changed

18 files changed

+356
-6
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Language Features:
99
Compiler Features:
1010
* Code Generator: More efficient overflow checks for multiplication.
1111
* Yul Optimizer: Simplify the starting offset of zero-length operations to zero.
12+
* 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).
1213

1314

1415
Bugfixes:

libsolidity/lsp/FileRepository.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
// SPDX-License-Identifier: GPL-3.0
1818

1919
#include <libsolidity/lsp/FileRepository.h>
20+
#include <libsolidity/lsp/Transport.h>
2021
#include <libsolidity/lsp/Utils.h>
2122

2223
#include <libsolutil/StringUtils.h>
@@ -25,11 +26,14 @@
2526
#include <range/v3/algorithm/none_of.hpp>
2627
#include <range/v3/range/conversion.hpp>
2728
#include <range/v3/view/transform.hpp>
29+
#include <boost/algorithm/string/predicate.hpp>
2830

2931
#include <regex>
3032

3133
#include <boost/algorithm/string/predicate.hpp>
3234

35+
#include <fmt/format.h>
36+
3337
using namespace std;
3438
using namespace solidity;
3539
using namespace solidity::lsp;
@@ -84,6 +88,7 @@ string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const
8488

8589
string FileRepository::uriToSourceUnitName(string const& _path) const
8690
{
91+
lspAssert(boost::algorithm::starts_with(_path, "file://"), ErrorCode::InternalError, "URI must start with file://");
8792
return stripFileUriSchemePrefix(_path);
8893
}
8994

@@ -92,6 +97,7 @@ void FileRepository::setSourceByUri(string const& _uri, string _source)
9297
// This is needed for uris outside the base path. It can lead to collisions,
9398
// but we need to mostly rewrite this in a future version anyway.
9499
auto sourceUnitName = uriToSourceUnitName(_uri);
100+
lspDebug(fmt::format("FileRepository.setSourceByUri({}): {}", _uri, _source));
95101
m_sourceUnitNamesToUri.emplace(sourceUnitName, _uri);
96102
m_sourceCodes[sourceUnitName] = std::move(_source);
97103
}

libsolidity/lsp/LanguageServer.cpp

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <liblangutil/SourceReferenceExtractor.h>
3333
#include <liblangutil/CharStream.h>
3434

35+
#include <libsolutil/CommonIO.h>
3536
#include <libsolutil/Visitor.h>
3637
#include <libsolutil/JSON.h>
3738

@@ -42,6 +43,8 @@
4243
#include <ostream>
4344
#include <string>
4445

46+
#include <fmt/format.h>
47+
4548
using namespace std;
4649
using namespace std::string_literals;
4750
using namespace std::placeholders;
@@ -50,9 +53,25 @@ using namespace solidity::lsp;
5053
using namespace solidity::langutil;
5154
using namespace solidity::frontend;
5255

56+
namespace fs = boost::filesystem;
57+
5358
namespace
5459
{
5560

61+
bool resolvesToRegularFile(boost::filesystem::path _path, int maxRecursionDepth = 10)
62+
{
63+
fs::file_status fileStatus = fs::status(_path);
64+
65+
while (fileStatus.type() == fs::file_type::symlink_file && maxRecursionDepth > 0)
66+
{
67+
_path = boost::filesystem::read_symlink(_path);
68+
fileStatus = fs::status(_path);
69+
maxRecursionDepth--;
70+
}
71+
72+
return fileStatus.type() == fs::file_type::regular_file;
73+
}
74+
5675
int toDiagnosticSeverity(Error::Type _errorType)
5776
{
5877
// 1=Error, 2=Warning, 3=Info, 4=Hint
@@ -118,7 +137,7 @@ LanguageServer::LanguageServer(Transport& _transport):
118137
{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}},
119138
{"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }},
120139
{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)},
121-
{"initialized", [](auto, auto) {}},
140+
{"initialized", bind(&LanguageServer::handleInitialized, this, _1, _2)},
122141
{"$/setTrace", [this](auto, Json::Value const& args) { setTrace(args["value"]); }},
123142
{"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }},
124143
{"textDocument/definition", GotoDefinition(*this) },
@@ -147,6 +166,26 @@ Json::Value LanguageServer::toJson(SourceLocation const& _location)
147166

148167
void LanguageServer::changeConfiguration(Json::Value const& _settings)
149168
{
169+
// The settings item: "file-load-strategy" (enum) defaults to "project-directory" if not (or not correctly) set.
170+
// It can be overridden during client's handshake or at runtime, as usual.
171+
//
172+
// 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.
173+
//
174+
// Operations include compiler analysis, but also finding all symbolic references or symbolic renaming.
175+
//
176+
// If this value is set to "directly-opened-and-on-import", then only currently directly opened files and
177+
// those files being imported directly or indirectly will be included in operations.
178+
if (_settings["file-load-strategy"])
179+
{
180+
auto const text = _settings["file-load-strategy"].asString();
181+
if (text == "project-directory")
182+
m_fileLoadStrategy = FileLoadStrategy::ProjectDirectory;
183+
else if (text == "directly-opened-and-on-import")
184+
m_fileLoadStrategy = FileLoadStrategy::DirectlyOpenedAndOnImported;
185+
else
186+
lspAssert(false, ErrorCode::InvalidParams, "Invalid file load strategy: " + text);
187+
}
188+
150189
m_settingsObject = _settings;
151190
Json::Value jsonIncludePaths = _settings["include-paths"];
152191

@@ -173,6 +212,24 @@ void LanguageServer::changeConfiguration(Json::Value const& _settings)
173212
}
174213
}
175214

215+
vector<boost::filesystem::path> LanguageServer::allSolidityFilesFromProject() const
216+
{
217+
vector<fs::path> collectedPaths{};
218+
219+
// We explicitly decided against including all files from include paths but leave the possibility
220+
// open for a future PR to enable such a feature to be optionally enabled (default disabled).
221+
222+
auto directoryIterator = fs::recursive_directory_iterator(m_fileRepository.basePath(), fs::symlink_option::recurse);
223+
for (fs::directory_entry const& dirEntry: directoryIterator)
224+
if (
225+
dirEntry.path().extension() == ".sol" &&
226+
(dirEntry.status().type() == fs::file_type::regular_file || resolvesToRegularFile(dirEntry.path()))
227+
)
228+
collectedPaths.push_back(dirEntry.path());
229+
230+
return collectedPaths;
231+
}
232+
176233
void LanguageServer::compile()
177234
{
178235
// For files that are not open, we have to take changes on disk into account,
@@ -181,6 +238,18 @@ void LanguageServer::compile()
181238
FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths());
182239
swap(oldRepository, m_fileRepository);
183240

241+
// Load all solidity files from project.
242+
if (m_fileLoadStrategy == FileLoadStrategy::ProjectDirectory)
243+
for (auto const& projectFile: allSolidityFilesFromProject())
244+
{
245+
lspDebug(fmt::format("adding project file: {}", projectFile.generic_string()));
246+
m_fileRepository.setSourceByUri(
247+
m_fileRepository.sourceUnitNameToUri(projectFile.generic_string()),
248+
util::readFileAsString(projectFile)
249+
);
250+
}
251+
252+
// Overwrite all files as opened by the client, including the ones which might potentially have changes.
184253
for (string const& fileName: m_openFiles)
185254
m_fileRepository.setSourceByUri(
186255
fileName,
@@ -269,6 +338,7 @@ bool LanguageServer::run()
269338
{
270339
string const methodName = (*jsonMessage)["method"].asString();
271340
id = (*jsonMessage)["id"];
341+
lspDebug(fmt::format("received method call: {}", methodName));
272342

273343
if (auto handler = util::valueOrDefault(m_handlers, methodName))
274344
handler(id, (*jsonMessage)["params"]);
@@ -278,6 +348,10 @@ bool LanguageServer::run()
278348
else
279349
m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string.");
280350
}
351+
catch (Json::Exception const&)
352+
{
353+
m_client.error(id, ErrorCode::InvalidParams, "JSON object access error. Most likely due to a badly formatted JSON request message."s);
354+
}
281355
catch (RequestError const& error)
282356
{
283357
m_client.error(id, error.code(), error.comment() ? *error.comment() : ""s);
@@ -347,6 +421,12 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
347421
m_client.reply(_id, move(replyArgs));
348422
}
349423

424+
void LanguageServer::handleInitialized(MessageID, Json::Value const&)
425+
{
426+
if (m_fileLoadStrategy == FileLoadStrategy::ProjectDirectory)
427+
compileAndUpdateDiagnostics();
428+
}
429+
350430
void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args)
351431
{
352432
auto uri = _args["textDocument"]["uri"];

libsolidity/lsp/LanguageServer.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,23 @@ namespace solidity::lsp
3636
class RenameSymbol;
3737
enum class ErrorCode;
3838

39+
/**
40+
* Enum to mandate what files to take into consideration for source code analysis.
41+
*/
42+
enum class FileLoadStrategy
43+
{
44+
/// Takes only those files into consideration that are explicitly opened and those
45+
/// that have been directly or indirectly imported.
46+
DirectlyOpenedAndOnImported = 0,
47+
48+
/// Takes all Solidity (.sol) files within the project root into account.
49+
/// Symbolic links will be followed, even if they lead outside of the project directory
50+
/// (`--allowed-paths` is currently ignored by the LSP).
51+
///
52+
/// This resembles the closest what other LSPs should be doing already.
53+
ProjectDirectory = 1,
54+
};
55+
3956
/**
4057
* Solidity Language Server, managing one LSP client.
4158
* This implements a subset of LSP version 3.16 that can be found at:
@@ -68,6 +85,7 @@ class LanguageServer
6885
/// Reports an error and returns false if not.
6986
void requireServerInitialized();
7087
void handleInitialize(MessageID _id, Json::Value const& _args);
88+
void handleInitialized(MessageID _id, Json::Value const& _args);
7189
void handleWorkspaceDidChangeConfiguration(Json::Value const& _args);
7290
void setTrace(Json::Value const& _args);
7391
void handleTextDocumentDidOpen(Json::Value const& _args);
@@ -82,6 +100,9 @@ class LanguageServer
82100

83101
/// Compile everything until after analysis phase.
84102
void compile();
103+
104+
std::vector<boost::filesystem::path> allSolidityFilesFromProject() const;
105+
85106
using MessageHandler = std::function<void(MessageID, Json::Value const&)>;
86107

87108
Json::Value toRange(langutil::SourceLocation const& _location);
@@ -100,6 +121,7 @@ class LanguageServer
100121
/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
101122
std::set<std::string> m_nonemptyDiagnostics;
102123
FileRepository m_fileRepository;
124+
FileLoadStrategy m_fileLoadStrategy = FileLoadStrategy::ProjectDirectory;
103125

104126
frontend::CompilerStack m_compilerStack;
105127

libsolidity/lsp/Transport.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
// SPDX-License-Identifier: GPL-3.0
1818
#include <libsolidity/lsp/Transport.h>
19+
#include <libsolidity/lsp/Utils.h>
1920

2021
#include <libsolutil/JSON.h>
2122
#include <libsolutil/Visitor.h>
@@ -205,11 +206,13 @@ std::string StdioTransport::getline()
205206
{
206207
std::string line;
207208
std::getline(std::cin, line);
209+
lspDebug(fmt::format("Received: {}", line));
208210
return line;
209211
}
210212

211213
void StdioTransport::writeBytes(std::string_view _data)
212214
{
215+
lspDebug(fmt::format("Sending: {}", _data));
213216
auto const bytesWritten = fwrite(_data.data(), 1, _data.size(), stdout);
214217
solAssert(bytesWritten == _data.size());
215218
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
contract C
5+
{
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
contract D
5+
{
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
contract E
5+
{
6+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
import "otherlib/second.sol";
5+
6+
contract C
7+
{
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
contract B
5+
{
6+
}

0 commit comments

Comments
 (0)