Skip to content

Commit f686294

Browse files
Merge pull request #12994 from ethereum/lsp-include-path
LSP to resolve custom include paths (e.g. Hardhat)
2 parents d003400 + d89008d commit f686294

File tree

12 files changed

+211
-56
lines changed

12 files changed

+211
-56
lines changed

Changelog.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ Compiler Features:
3030
* LSP: Add rudimentary support for semantic highlighting.
3131
* Type Checker: Warn about assignments involving multiple pushes to storage ``bytes`` that may invalidate references.
3232
* Yul Optimizer: Improve inlining heuristics for via IR code generation and pure Yul compilation.
33-
33+
* Language Server: Always add ``{project_root}/node_modules`` to include search paths.
34+
* Language Server: Adds support for configuring ``include-paths`` JSON settings object that can be passed during LSP configuration stage.
3435

3536
Bugfixes:
3637
* ABI Encoder: When encoding an empty string coming from storage do not add a superfluous empty slot for data.

libsolidity/lsp/FileRepository.cpp

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -22,37 +22,64 @@
2222
#include <libsolutil/StringUtils.h>
2323
#include <libsolutil/CommonIO.h>
2424

25+
#include <range/v3/algorithm/none_of.hpp>
2526
#include <range/v3/range/conversion.hpp>
2627
#include <range/v3/view/transform.hpp>
2728

2829
#include <regex>
2930

31+
#include <boost/algorithm/string/predicate.hpp>
32+
3033
using namespace std;
3134
using namespace solidity;
3235
using namespace solidity::lsp;
3336
using namespace solidity::frontend;
3437

3538
using solidity::util::readFileAsString;
3639
using solidity::util::joinHumanReadable;
40+
using solidity::util::Result;
3741

38-
FileRepository::FileRepository(boost::filesystem::path _basePath): m_basePath(std::move(_basePath))
42+
FileRepository::FileRepository(boost::filesystem::path _basePath, std::vector<boost::filesystem::path> _includePaths):
43+
m_basePath(std::move(_basePath)),
44+
m_includePaths(std::move(_includePaths))
3945
{
4046
}
4147

48+
void FileRepository::setIncludePaths(std::vector<boost::filesystem::path> _paths)
49+
{
50+
m_includePaths = std::move(_paths);
51+
}
52+
4253
string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const
4354
{
4455
regex const windowsDriveLetterPath("^[a-zA-Z]:/");
4556

57+
auto const ensurePathIsUnixLike = [&](string inputPath) -> string {
58+
if (!regex_search(inputPath, windowsDriveLetterPath))
59+
return inputPath;
60+
else
61+
return "/" + move(inputPath);
62+
};
63+
4664
if (m_sourceUnitNamesToUri.count(_sourceUnitName))
65+
{
66+
solAssert(boost::starts_with(m_sourceUnitNamesToUri.at(_sourceUnitName), "file://"), "");
4767
return m_sourceUnitNamesToUri.at(_sourceUnitName);
68+
}
4869
else if (_sourceUnitName.find("file://") == 0)
4970
return _sourceUnitName;
5071
else if (regex_search(_sourceUnitName, windowsDriveLetterPath))
5172
return "file:///" + _sourceUnitName;
52-
else if (_sourceUnitName.find("/") == 0)
53-
return "file://" + _sourceUnitName;
54-
else
73+
else if (
74+
auto const resolvedPath = tryResolvePath(_sourceUnitName);
75+
resolvedPath.message().empty()
76+
)
77+
return "file://" + ensurePathIsUnixLike(resolvedPath.get().generic_string());
78+
else if (m_basePath.generic_string() != "/")
5579
return "file://" + m_basePath.generic_string() + "/" + _sourceUnitName;
80+
else
81+
// Avoid double-/ in case base-path itself is simply a UNIX root filesystem root.
82+
return "file:///" + _sourceUnitName;
5683
}
5784

5885
string FileRepository::uriToSourceUnitName(string const& _path) const
@@ -69,6 +96,52 @@ void FileRepository::setSourceByUri(string const& _uri, string _source)
6996
m_sourceCodes[sourceUnitName] = std::move(_source);
7097
}
7198

99+
Result<boost::filesystem::path> FileRepository::tryResolvePath(std::string const& _strippedSourceUnitName) const
100+
{
101+
if (
102+
boost::filesystem::path(_strippedSourceUnitName).has_root_path() &&
103+
boost::filesystem::exists(_strippedSourceUnitName)
104+
)
105+
return boost::filesystem::path(_strippedSourceUnitName);
106+
107+
vector<boost::filesystem::path> candidates;
108+
vector<reference_wrapper<boost::filesystem::path const>> prefixes = {m_basePath};
109+
prefixes += (m_includePaths | ranges::to<vector<reference_wrapper<boost::filesystem::path const>>>);
110+
auto const defaultInclude = m_basePath / "node_modules";
111+
if (m_includePaths.empty())
112+
prefixes.emplace_back(defaultInclude);
113+
114+
auto const pathToQuotedString = [](boost::filesystem::path const& _path) { return "\"" + _path.string() + "\""; };
115+
116+
for (auto const& prefix: prefixes)
117+
{
118+
boost::filesystem::path canonicalPath = boost::filesystem::path(prefix) / boost::filesystem::path(_strippedSourceUnitName);
119+
120+
if (boost::filesystem::exists(canonicalPath))
121+
candidates.push_back(move(canonicalPath));
122+
}
123+
124+
if (candidates.empty())
125+
return Result<boost::filesystem::path>::err(
126+
"File not found. Searched the following locations: " +
127+
joinHumanReadable(prefixes | ranges::views::transform(pathToQuotedString), ", ") +
128+
"."
129+
);
130+
131+
if (candidates.size() >= 2)
132+
return Result<boost::filesystem::path>::err(
133+
"Ambiguous import. "
134+
"Multiple matching files found inside base path and/or include paths: " +
135+
joinHumanReadable(candidates | ranges::views::transform(pathToQuotedString), ", ") +
136+
"."
137+
);
138+
139+
if (!boost::filesystem::is_regular_file(candidates[0]))
140+
return Result<boost::filesystem::path>::err("Not a valid file.");
141+
142+
return candidates[0];
143+
}
144+
72145
frontend::ReadCallback::Result FileRepository::readFile(string const& _kind, string const& _sourceUnitName)
73146
{
74147
solAssert(
@@ -83,53 +156,11 @@ frontend::ReadCallback::Result FileRepository::readFile(string const& _kind, str
83156
return ReadCallback::Result{true, m_sourceCodes.at(_sourceUnitName)};
84157

85158
string const strippedSourceUnitName = stripFileUriSchemePrefix(_sourceUnitName);
159+
Result<boost::filesystem::path> const resolvedPath = tryResolvePath(strippedSourceUnitName);
160+
if (!resolvedPath.message().empty())
161+
return ReadCallback::Result{false, resolvedPath.message()};
86162

87-
if (
88-
boost::filesystem::path(strippedSourceUnitName).has_root_path() &&
89-
boost::filesystem::exists(strippedSourceUnitName)
90-
)
91-
{
92-
auto contents = readFileAsString(strippedSourceUnitName);
93-
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
94-
m_sourceCodes[_sourceUnitName] = contents;
95-
return ReadCallback::Result{true, move(contents)};
96-
}
97-
98-
vector<boost::filesystem::path> candidates;
99-
vector<reference_wrapper<boost::filesystem::path>> prefixes = {m_basePath};
100-
prefixes += (m_includePaths | ranges::to<vector<reference_wrapper<boost::filesystem::path>>>);
101-
102-
auto const pathToQuotedString = [](boost::filesystem::path const& _path) { return "\"" + _path.string() + "\""; };
103-
104-
for (auto const& prefix: prefixes)
105-
{
106-
boost::filesystem::path canonicalPath = boost::filesystem::path(prefix) / boost::filesystem::path(strippedSourceUnitName);
107-
108-
if (boost::filesystem::exists(canonicalPath))
109-
candidates.push_back(move(canonicalPath));
110-
}
111-
112-
if (candidates.empty())
113-
return ReadCallback::Result{
114-
false,
115-
"File not found. Searched the following locations: " +
116-
joinHumanReadable(prefixes | ranges::views::transform(pathToQuotedString), ", ") +
117-
"."
118-
};
119-
120-
if (candidates.size() >= 2)
121-
return ReadCallback::Result{
122-
false,
123-
"Ambiguous import. "
124-
"Multiple matching files found inside base path and/or include paths: " +
125-
joinHumanReadable(candidates | ranges::views::transform(pathToQuotedString), ", ") +
126-
"."
127-
};
128-
129-
if (!boost::filesystem::is_regular_file(candidates[0]))
130-
return ReadCallback::Result{false, "Not a valid file."};
131-
132-
auto contents = readFileAsString(candidates[0]);
163+
auto contents = readFileAsString(resolvedPath.get());
133164
solAssert(m_sourceCodes.count(_sourceUnitName) == 0, "");
134165
m_sourceCodes[_sourceUnitName] = contents;
135166
return ReadCallback::Result{true, move(contents)};

libsolidity/lsp/FileRepository.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#pragma once
1919

2020
#include <libsolidity/interface/FileReader.h>
21+
#include <libsolutil/Result.h>
2122

2223
#include <string>
2324
#include <map>
@@ -28,7 +29,10 @@ namespace solidity::lsp
2829
class FileRepository
2930
{
3031
public:
31-
explicit FileRepository(boost::filesystem::path _basePath);
32+
FileRepository(boost::filesystem::path _basePath, std::vector<boost::filesystem::path> _includePaths);
33+
34+
std::vector<boost::filesystem::path> const& includePaths() const noexcept { return m_includePaths; }
35+
void setIncludePaths(std::vector<boost::filesystem::path> _paths);
3236

3337
boost::filesystem::path const& basePath() const { return m_basePath; }
3438

@@ -51,6 +55,8 @@ class FileRepository
5155
return [this](std::string const& _kind, std::string const& _path) { return readFile(_kind, _path); };
5256
}
5357

58+
util::Result<boost::filesystem::path> tryResolvePath(std::string const& _sourceUnitName) const;
59+
5460
private:
5561
/// Base path without URI scheme.
5662
boost::filesystem::path m_basePath;

libsolidity/lsp/LanguageServer.cpp

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ LanguageServer::LanguageServer(Transport& _transport):
130130
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
131131
{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)},
132132
},
133-
m_fileRepository("/" /* basePath */),
133+
m_fileRepository("/" /* basePath */, {} /* no search paths */),
134134
m_compilerStack{m_fileRepository.reader()}
135135
{
136136
}
@@ -148,14 +148,34 @@ Json::Value LanguageServer::toJson(SourceLocation const& _location)
148148
void LanguageServer::changeConfiguration(Json::Value const& _settings)
149149
{
150150
m_settingsObject = _settings;
151+
Json::Value jsonIncludePaths = _settings["include-paths"];
152+
int typeFailureCount = 0;
153+
154+
if (jsonIncludePaths && jsonIncludePaths.isArray())
155+
{
156+
vector<boost::filesystem::path> includePaths;
157+
for (Json::Value const& jsonPath: jsonIncludePaths)
158+
{
159+
if (jsonPath.isString())
160+
includePaths.emplace_back(boost::filesystem::path(jsonPath.asString()));
161+
else
162+
typeFailureCount++;
163+
}
164+
m_fileRepository.setIncludePaths(move(includePaths));
165+
}
166+
else
167+
++typeFailureCount;
168+
169+
if (typeFailureCount)
170+
m_client.trace("Invalid JSON configuration passed. \"include-paths\" must be an array of strings.");
151171
}
152172

153173
void LanguageServer::compile()
154174
{
155175
// For files that are not open, we have to take changes on disk into account,
156176
// so we just remove all non-open files.
157177

158-
FileRepository oldRepository(m_fileRepository.basePath());
178+
FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths());
159179
swap(oldRepository, m_fileRepository);
160180

161181
for (string const& fileName: m_openFiles)
@@ -302,7 +322,7 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
302322
else if (Json::Value rootPath = _args["rootPath"])
303323
rootPath = rootPath.asString();
304324

305-
m_fileRepository = FileRepository(rootPath);
325+
m_fileRepository = FileRepository(rootPath, {});
306326
if (_args["initializationOptions"].isObject())
307327
changeConfiguration(_args["initializationOptions"]);
308328

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
import "my-module/test.sol";
5+
6+
contract MyContract
7+
{
8+
function f(uint a, uint b) public pure returns (uint)
9+
{
10+
return MyModule.add(a, b);
11+
}
12+
}
13+
// ----
14+
// test:
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
import "rootlib.sol";
5+
6+
contract MyContract
7+
{
8+
}
9+
// ----
10+
// rootlib:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
import "test.sol";
5+
// ^^^^^^^^^^^^^^^^^^ @IncludeLocation
6+
7+
contract SomeContract
8+
{
9+
}
10+
// ----
11+
// file_not_found_in_searchpath: @IncludeLocation 6275
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity >=0.8.0;
3+
4+
import "otherlib/otherlib.sol";
5+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @NotFound
6+
7+
contract MyContract
8+
{
9+
}
10+
// ----
11+
// using-custom-includes: @NotFound 6275

test/libsolidity/lsp/node_modules/my-module/test.sol

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/libsolidity/lsp/node_modules/rootlib.sol

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)