diff --git a/Changelog.md b/Changelog.md index 68c755734a6f..cfff30a86f9c 100644 --- a/Changelog.md +++ b/Changelog.md @@ -9,6 +9,7 @@ Compiler Features: * Standard JSON: Add a boolean field `settings.metadata.appendCBOR` that skips CBOR metadata from getting appended at the end of the bytecode. * Yul Optimizer: Allow replacing the previously hard-coded cleanup sequence by specifying custom steps after a colon delimiter (``:``) in the sequence string. * Language Server: Add basic document hover support. +* Language Server: Add configuration option to apply custom remappings via ``remappings`` in the LSP settings object. Bugfixes: diff --git a/libsolidity/lsp/FileRepository.cpp b/libsolidity/lsp/FileRepository.cpp index ca40343af3a5..8348566dbea6 100644 --- a/libsolidity/lsp/FileRepository.cpp +++ b/libsolidity/lsp/FileRepository.cpp @@ -16,6 +16,7 @@ */ // SPDX-License-Identifier: GPL-3.0 +#include #include #include #include @@ -43,9 +44,10 @@ using solidity::util::readFileAsString; using solidity::util::joinHumanReadable; using solidity::util::Result; -FileRepository::FileRepository(boost::filesystem::path _basePath, std::vector _includePaths): +FileRepository::FileRepository(boost::filesystem::path _basePath, std::vector _includePaths, vector _remappings): m_basePath(std::move(_basePath)), - m_includePaths(std::move(_includePaths)) + m_includePaths(std::move(_includePaths)), + m_remappings(std::move(_remappings)) { } @@ -54,6 +56,11 @@ void FileRepository::setIncludePaths(std::vector _paths m_includePaths = std::move(_paths); } +void FileRepository::setRemappings(vector _remappings) +{ + m_remappings = std::move(_remappings); +} + string FileRepository::sourceUnitNameToUri(string const& _sourceUnitName) const { regex const windowsDriveLetterPath("^[a-zA-Z]:/"); diff --git a/libsolidity/lsp/FileRepository.h b/libsolidity/lsp/FileRepository.h index dfd9319383e2..2560ed37ca56 100644 --- a/libsolidity/lsp/FileRepository.h +++ b/libsolidity/lsp/FileRepository.h @@ -18,6 +18,7 @@ #pragma once #include +#include #include #include @@ -29,11 +30,14 @@ namespace solidity::lsp class FileRepository { public: - FileRepository(boost::filesystem::path _basePath, std::vector _includePaths); + FileRepository(boost::filesystem::path _basePath, std::vector _includePaths, std::vector _remappings); std::vector const& includePaths() const noexcept { return m_includePaths; } void setIncludePaths(std::vector _paths); + std::vector const& remappings() const noexcept { return m_remappings; } + void setRemappings(std::vector _remappings); + boost::filesystem::path const& basePath() const { return m_basePath; } /// Translates a compiler-internal source unit name to an LSP client path. @@ -64,6 +68,9 @@ class FileRepository /// Additional directories used for resolving relative paths in imports. std::vector m_includePaths; + /// Remappings of imports. + std::vector m_remappings; + /// Mapping of source unit names to their URIs as understood by the client. StringMap m_sourceUnitNamesToUri; diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index 74d1b4d7ccef..e6170b2ffec8 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -151,7 +152,7 @@ LanguageServer::LanguageServer(Transport& _transport): {"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)}, {"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)}, }, - m_fileRepository("/" /* basePath */, {} /* no search paths */), + m_fileRepository("/" /* basePath */, {} /* no search paths */, {} /* no remappings */), m_compilerStack{m_fileRepository.reader()} { } @@ -212,6 +213,40 @@ void LanguageServer::changeConfiguration(Json::Value const& _settings) if (typeFailureCount) m_client.trace("Invalid JSON configuration passed. \"include-paths\" must be an array of strings."); } + + Json::Value jsonRemappings = _settings["remappings"]; + + if (jsonRemappings) + { + bool typeFailures = false; + if (jsonRemappings.isArray()) + { + vector remappings; + for (Json::Value const& jsonPath: jsonRemappings) + { + if ( + jsonPath.isObject() && + jsonPath.get("context", "").isString() && + jsonPath.get("prefix", "").isString() && + jsonPath.get("target", "").isString() + ) + remappings.emplace_back(ImportRemapper::Remapping{ + jsonPath.get("context", "").asString(), + jsonPath.get("prefix", "").asString(), + jsonPath.get("target", "").asString(), + }); + else + typeFailures = true; + } + m_fileRepository.setRemappings(std::move(remappings)); + } + else + typeFailures = true; + + if (typeFailures) + m_client.trace("Invalid JSON configuration passed. \"remappings\" should be of form [{\"context\":\"\", \"prefix\":\"foo\", \"target\":\"bar\"}]."); + + } } vector LanguageServer::allSolidityFilesFromProject() const @@ -237,7 +272,7 @@ void LanguageServer::compile() // For files that are not open, we have to take changes on disk into account, // so we just remove all non-open files. - FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths()); + FileRepository oldRepository(m_fileRepository.basePath(), m_fileRepository.includePaths(), m_fileRepository.remappings()); swap(oldRepository, m_fileRepository); // Load all solidity files from project. @@ -261,6 +296,7 @@ void LanguageServer::compile() // TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty). m_compilerStack.reset(false); + m_compilerStack.setRemappings(m_fileRepository.remappings()); m_compilerStack.setSources(m_fileRepository.sourceUnits()); m_compilerStack.compile(CompilerStack::State::AnalysisPerformed); } @@ -404,7 +440,7 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) if (_args["trace"]) setTrace(_args["trace"]); - m_fileRepository = FileRepository(rootPath, {}); + m_fileRepository = FileRepository(rootPath, {}, {}); if (_args["initializationOptions"].isObject()) changeConfiguration(_args["initializationOptions"]); diff --git a/test/lsp.py b/test/lsp.py index 2e097dab0a28..b4569a13ff92 100755 --- a/test/lsp.py +++ b/test/lsp.py @@ -1085,6 +1085,13 @@ def open_file_and_wait_for_diagnostics( ) return self.wait_for_diagnostics(solc_process) + def expect_report(self, published_diagnostics, uri): + for report in published_diagnostics: + if report['uri'] == uri: + return report + self.expect_true(False, f"expected ${uri} in published_diagnostics") + return None + def expect_true( self, actual, @@ -1534,6 +1541,70 @@ def test_custom_includes(self, solc: JsonRpcProcess) -> None: self.expect_equal(len(diagnostics), 1, "no diagnostics") self.expect_diagnostic(diagnostics[0], code=2018, lineNo=5, startEndColumns=(4, 62)) + def test_remapping_wrong_paramateres(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc, expose_project_root=False) + solc.send_notification( + 'workspace/didChangeConfiguration', { + 'settings': { + 'remappings': [ + { + 'context': False, + 'prefix': 1, + 'target': f"{self.project_root_dir}/other-include-dir/otherlib/" + } + ] + } + } + ) + + response = solc.receive_message() + self.expect_equal(response["method"], "$/logTrace") + + if not response["params"]["message"].startswith("Invalid JSON configuration passed."): + raise Exception("Expected JSON error.") + + def test_remapping(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc, expose_project_root=False) + solc.send_notification( + 'workspace/didChangeConfiguration', { + 'settings': { + 'remappings': [ + { + 'context': '', + 'prefix': '@other/', + 'target': f"{self.project_root_dir}/other-include-dir/otherlib/" + } + ] + } + } + ) + + # test file + file_with_remapped_import_uri = 'file:///remapped-include.sol' + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': file_with_remapped_import_uri, + 'languageId': 'Solidity', + 'version': 1, + 'text': ''.join([ + '// SPDX-License-Identifier: UNLICENSED\n', + 'pragma solidity >=0.8.0;\n', + 'import "@other/otherlib.sol";\n', + ]) + } + }) + published_diagnostics = self.wait_for_diagnostics(solc) + self.expect_equal(len(published_diagnostics), 2, "expected reports for two files") + + report = self.expect_report(published_diagnostics, file_with_remapped_import_uri) + diagnostics = report['diagnostics'] + self.expect_equal(len(diagnostics), 0, "no diagnostics") + + report = self.expect_report(published_diagnostics, f"{self.project_root_uri}/other-include-dir/otherlib/otherlib.sol") + diagnostics = report['diagnostics'] + self.expect_equal(len(diagnostics), 1, "no diagnostics") + 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.