diff --git a/Changelog.md b/Changelog.md index 39d22c88e..3e38e7992 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ Compiler Features: * JSON-AST: Added selector field for errors and events. * Peephole Optimizer: Optimize comparisons in front of conditional jumps and conditional jumps across a single unconditional jump. * Yul Optimizer: Remove ``sstore`` and ``mstore`` operations that are never read from. + * LSP: Implements goto-definition. Bugfixes: * Yul IR Code Generation: Optimize embedded creation code with correct settings. This fixes potential mismatches between the constructor code of a contract compiled in isolation and the bytecode in ``type(C).creationCode``, resp. the bytecode used for ``new C(...)``. diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index 04a4300a3..3d2845463 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -155,10 +155,10 @@ set(sources interface/StorageLayout.h interface/Version.cpp interface/Version.h - lsp/LanguageServer.cpp - lsp/LanguageServer.h lsp/FileRepository.cpp lsp/FileRepository.h + lsp/GotoDefinition.cpp + lsp/GotoDefinition.h lsp/HandlerBase.cpp lsp/HandlerBase.h lsp/LanguageServer.cpp diff --git a/libsolidity/ast/ASTUtils.cpp b/libsolidity/ast/ASTUtils.cpp index 3ed47d6f2..2b7ea7427 100644 --- a/libsolidity/ast/ASTUtils.cpp +++ b/libsolidity/ast/ASTUtils.cpp @@ -18,12 +18,35 @@ #include #include +#include #include namespace solidity::frontend { +ASTNode const* locateInnermostASTNode(int _offsetInFile, SourceUnit const& _sourceUnit) +{ + ASTNode const* innermostMatch = nullptr; + auto locator = SimpleASTVisitor( + [&](ASTNode const& _node) -> bool + { + // In the AST parent location always covers the whole child location. + // The parent is visited first so to get the innermost node we simply + // take the last one that still contains the offset. + + if (!_node.location().containsOffset(_offsetInFile)) + return false; + + innermostMatch = &_node; + return true; + }, + [](ASTNode const&) {} + ); + _sourceUnit.accept(locator); + return innermostMatch; +} + bool isConstantVariableRecursive(VariableDeclaration const& _varDecl) { solAssert(_varDecl.isConstant(), "Constant variable expected"); diff --git a/libsolidity/ast/ASTUtils.h b/libsolidity/ast/ASTUtils.h index 5b1a7d4e5..2bfbb8fe9 100644 --- a/libsolidity/ast/ASTUtils.h +++ b/libsolidity/ast/ASTUtils.h @@ -21,9 +21,11 @@ namespace solidity::frontend { -class VariableDeclaration; +class ASTNode; class Declaration; class Expression; +class SourceUnit; +class VariableDeclaration; /// Find the topmost referenced constant variable declaration when the given variable /// declaration value is an identifier. Works only for constant variable declarations. @@ -33,4 +35,7 @@ VariableDeclaration const* rootConstVariableDeclaration(VariableDeclaration cons /// Returns true if the constant variable declaration is recursive. bool isConstantVariableRecursive(VariableDeclaration const& _varDecl); +/// Returns the innermost AST node that covers the given location or nullptr if not found. +ASTNode const* locateInnermostASTNode(int _offsetInFile, SourceUnit const& _sourceUnit); + } diff --git a/libsolidity/lsp/GotoDefinition.cpp b/libsolidity/lsp/GotoDefinition.cpp new file mode 100644 index 000000000..26fb686db --- /dev/null +++ b/libsolidity/lsp/GotoDefinition.cpp @@ -0,0 +1,66 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include +#include // for RequestError +#include +#include +#include + +#include + +#include +#include +#include + +using namespace solidity::frontend; +using namespace solidity::langutil; +using namespace solidity::lsp; +using namespace std; + +void GotoDefinition::operator()(MessageID _id, Json::Value const& _args) +{ + auto const [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args); + + ASTNode const* sourceNode = m_server.astNodeAtSourceLocation(sourceUnitName, lineColumn); + + vector locations; + if (auto const* expression = dynamic_cast(sourceNode)) + { + // Handles all expressions that can have one or more declaration annotation. + if (auto const* declaration = referencedDeclaration(expression)) + if (auto location = declarationLocation(declaration)) + locations.emplace_back(move(location.value())); + } + else if (auto const* identifierPath = dynamic_cast(sourceNode)) + { + if (auto const* declaration = identifierPath->annotation().referencedDeclaration) + if (auto location = declarationLocation(declaration)) + locations.emplace_back(move(location.value())); + } + else if (auto const* importDirective = dynamic_cast(sourceNode)) + { + auto const& path = *importDirective->annotation().absolutePath; + if (fileRepository().sourceUnits().count(path)) + locations.emplace_back(SourceLocation{0, 0, make_shared(path)}); + } + + Json::Value reply = Json::arrayValue; + for (SourceLocation const& location: locations) + reply.append(toJson(location)); + client().reply(_id, reply); +} diff --git a/libsolidity/lsp/GotoDefinition.h b/libsolidity/lsp/GotoDefinition.h new file mode 100644 index 000000000..453da3f15 --- /dev/null +++ b/libsolidity/lsp/GotoDefinition.h @@ -0,0 +1,31 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include + +namespace solidity::lsp +{ + +class GotoDefinition: public HandlerBase +{ +public: + explicit GotoDefinition(LanguageServer& _server): HandlerBase(_server) {} + + void operator()(MessageID, Json::Value const&); +}; + +} diff --git a/libsolidity/lsp/HandlerBase.cpp b/libsolidity/lsp/HandlerBase.cpp index 3ef227461..0da19aad3 100644 --- a/libsolidity/lsp/HandlerBase.cpp +++ b/libsolidity/lsp/HandlerBase.cpp @@ -1,3 +1,22 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include + #include #include #include @@ -5,13 +24,13 @@ #include +#include + +using namespace solidity::langutil; +using namespace solidity::lsp; +using namespace solidity::util; using namespace std; -namespace solidity::lsp -{ - -using namespace langutil; - Json::Value HandlerBase::toRange(SourceLocation const& _location) const { if (!_location.hasText()) @@ -33,31 +52,27 @@ Json::Value HandlerBase::toJson(SourceLocation const& _location) const return item; } -optional HandlerBase::parsePosition(string const& _sourceUnitName, Json::Value const& _position) const +pair HandlerBase::extractSourceUnitNameAndLineColumn(Json::Value const& _args) const { - if (!fileRepository().sourceUnits().count(_sourceUnitName)) - return nullopt; + string const uri = _args["textDocument"]["uri"].asString(); + string const sourceUnitName = fileRepository().clientPathToSourceUnitName(uri); + if (!fileRepository().sourceUnits().count(sourceUnitName)) + BOOST_THROW_EXCEPTION( + RequestError(ErrorCode::RequestFailed) << + errinfo_comment("Unknown file: " + uri) + ); - if (optional lineColumn = parseLineColumn(_position)) - if (optional const offset = CharStream::translateLineColumnToPosition( - fileRepository().sourceUnits().at(_sourceUnitName), - *lineColumn - )) - return SourceLocation{*offset, *offset, make_shared(_sourceUnitName)}; - return nullopt; -} - -optional HandlerBase::parseRange(string const& _sourceUnitName, Json::Value const& _range) const -{ - if (!_range.isObject()) - return nullopt; - optional start = parsePosition(_sourceUnitName, _range["start"]); - optional end = parsePosition(_sourceUnitName, _range["end"]); - if (!start || !end) - return nullopt; - solAssert(*start->sourceName == *end->sourceName); - start->end = end->end; - return start; -} + auto const lineColumn = parseLineColumn(_args["position"]); + if (!lineColumn) + BOOST_THROW_EXCEPTION( + RequestError(ErrorCode::RequestFailed) << + errinfo_comment(fmt::format( + "Unknown position {line}:{column} in file: {file}", + fmt::arg("line", lineColumn.value().line), + fmt::arg("column", lineColumn.value().column), + fmt::arg("file", sourceUnitName) + )) + ); + return {sourceUnitName, *lineColumn}; } diff --git a/libsolidity/lsp/HandlerBase.h b/libsolidity/lsp/HandlerBase.h index 067d485b5..3ca1679be 100644 --- a/libsolidity/lsp/HandlerBase.h +++ b/libsolidity/lsp/HandlerBase.h @@ -1,3 +1,20 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 #pragma once #include @@ -24,22 +41,15 @@ public: Json::Value toRange(langutil::SourceLocation const& _location) const; Json::Value toJson(langutil::SourceLocation const& _location) const; - std::optional parsePosition( - std::string const& _sourceUnitName, - Json::Value const& _position - ) const; + /// @returns source unit name and the line column position as extracted + /// from the JSON-RPC parameters. + std::pair extractSourceUnitNameAndLineColumn(Json::Value const& _params) const; - /// @returns the source location given a source unit name and an LSP Range object, - /// or nullopt on failure. - std::optional parseRange( - std::string const& _sourceUnitName, - Json::Value const& _range - ) const; - - langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.charStreamProvider(); }; - FileRepository const& fileRepository() const noexcept { return m_server.fileRepository(); }; - Transport& client() const noexcept { return m_server.client(); }; + langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.charStreamProvider(); } + FileRepository const& fileRepository() const noexcept { return m_server.fileRepository(); } + Transport& client() const noexcept { return m_server.client(); } +protected: LanguageServer& m_server; }; diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index 1808fc993..671476ab0 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -24,6 +24,8 @@ #include #include +// LSP feature implementations +#include #include #include @@ -75,9 +77,11 @@ LanguageServer::LanguageServer(Transport& _transport): {"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)}, {"initialized", [](auto, auto) {}}, {"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }}, + {"textDocument/definition", GotoDefinition(*this) }, {"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _2)}, {"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _2)}, {"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)}, + {"textDocument/implementation", GotoDefinition(*this) }, {"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)}, }, m_fileRepository("/" /* basePath */), @@ -85,11 +89,6 @@ LanguageServer::LanguageServer(Transport& _transport): { } -optional LanguageServer::parseRange(string const& _sourceUnitName, Json::Value const& _range) -{ - return HandlerBase{*this}.parseRange(_sourceUnitName, _range); -} - Json::Value LanguageServer::toRange(SourceLocation const& _location) { return HandlerBase(*this).toRange(_location); @@ -258,8 +257,10 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) Json::Value replyArgs; replyArgs["serverInfo"]["name"] = "solc"; replyArgs["serverInfo"]["version"] = string(VersionNumber); - replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; + replyArgs["capabilities"]["definitionProvider"] = true; + replyArgs["capabilities"]["implementationProvider"] = true; replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental + replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; m_client.reply(_id, move(replyArgs)); } @@ -313,7 +314,7 @@ void LanguageServer::handleTextDocumentDidChange(Json::Value const& _args) string text = jsonContentChange["text"].asString(); if (jsonContentChange["range"].isObject()) // otherwise full content update { - optional change = parseRange(sourceUnitName, jsonContentChange["range"]); + optional change = parseRange(m_fileRepository, sourceUnitName, jsonContentChange["range"]); lspAssert( change && change->hasText(), ErrorCode::RequestFailed, @@ -346,7 +347,7 @@ void LanguageServer::handleTextDocumentDidClose(Json::Value const& _args) compileAndUpdateDiagnostics(); } -ASTNode const* LanguageServer::requestASTNode(std::string const& _sourceUnitName, LineColumn const& _filePos) +ASTNode const* LanguageServer::astNodeAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos) { if (m_compilerStack.state() < CompilerStack::AnalysisPerformed) return nullptr; @@ -354,11 +355,10 @@ ASTNode const* LanguageServer::requestASTNode(std::string const& _sourceUnitName if (!m_fileRepository.sourceUnits().count(_sourceUnitName)) return nullptr; - optional sourcePos = m_compilerStack.charStream(_sourceUnitName) - .translateLineColumnToPosition(_filePos); - if (!sourcePos.has_value()) + if (optional sourcePos = + m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos)) + return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName)); + else return nullptr; - - return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName)); } diff --git a/libsolidity/lsp/LanguageServer.h b/libsolidity/lsp/LanguageServer.h index 8bc5e21a2..d2ad09367 100644 --- a/libsolidity/lsp/LanguageServer.h +++ b/libsolidity/lsp/LanguageServer.h @@ -59,7 +59,7 @@ public: FileRepository& fileRepository() noexcept { return m_fileRepository; } Transport& client() noexcept { return m_client; } - frontend::ASTNode const* requestASTNode(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos); + frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos); langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_compilerStack; } private: @@ -71,6 +71,7 @@ private: void handleTextDocumentDidOpen(Json::Value const& _args); void handleTextDocumentDidChange(Json::Value const& _args); void handleTextDocumentDidClose(Json::Value const& _args); + void handleGotoDefinition(MessageID _id, Json::Value const& _args); /// Invoked when the server user-supplied configuration changes (initiated by the client). void changeConfiguration(Json::Value const&); @@ -79,12 +80,6 @@ private: void compile(); using MessageHandler = std::function; - /// @returns the source location given a source unit name and an LSP Range object, - /// or nullopt on failure. - std::optional parseRange( - std::string const& _sourceUnitName, - Json::Value const& _range - ); Json::Value toRange(langutil::SourceLocation const& _location); Json::Value toJson(langutil::SourceLocation const& _location); diff --git a/libsolidity/lsp/Utils.cpp b/libsolidity/lsp/Utils.cpp index a25e06d5f..624d1f150 100644 --- a/libsolidity/lsp/Utils.cpp +++ b/libsolidity/lsp/Utils.cpp @@ -1,9 +1,30 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + #include #include #include #include #include +#include +#include + namespace solidity::lsp { @@ -19,7 +40,7 @@ optional parseLineColumn(Json::Value const& _lineColumn) return nullopt; } -Json::Value toJson(LineColumn _pos) +Json::Value toJson(LineColumn const& _pos) { Json::Value json = Json::objectValue; json["line"] = max(_pos.line, 0); @@ -36,24 +57,20 @@ Json::Value toJsonRange(LineColumn const& _start, LineColumn const& _end) return json; } -vector allAnnotatedDeclarations(Expression const* _expression) +Declaration const* referencedDeclaration(Expression const* _expression) { - vector output; - if (auto const* identifier = dynamic_cast(_expression)) - { - output.push_back(identifier->annotation().referencedDeclaration); - output += identifier->annotation().candidateDeclarations; - } - else if (auto const* memberAccess = dynamic_cast(_expression)) - { - output.push_back(memberAccess->annotation().referencedDeclaration); - } + if (Declaration const* referencedDeclaration = identifier->annotation().referencedDeclaration) + return referencedDeclaration; - return output; + if (auto const* memberAccess = dynamic_cast(_expression)) + if (memberAccess->annotation().referencedDeclaration) + return memberAccess->annotation().referencedDeclaration; + + return nullptr; } -optional declarationPosition(Declaration const* _declaration) +optional declarationLocation(Declaration const* _declaration) { if (!_declaration) return nullopt; @@ -67,4 +84,35 @@ optional declarationPosition(Declaration const* _declaration) return nullopt; } +optional parsePosition( + FileRepository const& _fileRepository, + string const& _sourceUnitName, + Json::Value const& _position +) +{ + if (!_fileRepository.sourceUnits().count(_sourceUnitName)) + return nullopt; + + if (optional lineColumn = parseLineColumn(_position)) + if (optional const offset = CharStream::translateLineColumnToPosition( + _fileRepository.sourceUnits().at(_sourceUnitName), + *lineColumn + )) + return SourceLocation{*offset, *offset, make_shared(_sourceUnitName)}; + return nullopt; +} + +optional parseRange(FileRepository const& _fileRepository, string const& _sourceUnitName, Json::Value const& _range) +{ + if (!_range.isObject()) + return nullopt; + optional start = parsePosition(_fileRepository, _sourceUnitName, _range["start"]); + optional end = parsePosition(_fileRepository, _sourceUnitName, _range["end"]); + if (!start || !end) + return nullopt; + solAssert(*start->sourceName == *end->sourceName); + start->end = end->end; + return start; +} + } diff --git a/libsolidity/lsp/Utils.h b/libsolidity/lsp/Utils.h index 8b598823f..3594efba2 100644 --- a/libsolidity/lsp/Utils.h +++ b/libsolidity/lsp/Utils.h @@ -1,3 +1,21 @@ +/* + This file is part of solidity. + + solidity is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + solidity is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with solidity. If not, see . +*/ +// SPDX-License-Identifier: GPL-3.0 + #pragma once #include @@ -9,6 +27,13 @@ #include #include +#if !defined(NDEBUG) +#include +#define lspDebug(message) (std::ofstream("/tmp/solc.log", std::ios::app) << (message) << std::endl) +#else +#define lspDebug(message) do {} while (0) +#endif + namespace solidity::langutil { class CharStreamProvider; @@ -20,10 +45,35 @@ namespace solidity::lsp class FileRepository; std::optional parseLineColumn(Json::Value const& _lineColumn); -Json::Value toJson(langutil::LineColumn _pos); +Json::Value toJson(langutil::LineColumn const& _pos); Json::Value toJsonRange(langutil::LineColumn const& _start, langutil::LineColumn const& _end); -std::vector allAnnotatedDeclarations(frontend::Expression const* _expression); -std::optional declarationPosition(frontend::Declaration const* _declaration); +/// @returns the source location given a source unit name and an LSP Range object, +/// or nullopt on failure. +std::optional parsePosition( + FileRepository const& _fileRepository, + std::string const& _sourceUnitName, + Json::Value const& _position +); + +/// @returns the source location given a source unit name and an LSP Range object, +/// or nullopt on failure. +std::optional parseRange( + FileRepository const& _fileRepository, + std::string const& _sourceUnitName, + Json::Value const& _range +); + +/// Extracts the resolved declaration of the given expression AST node. +/// +/// This may for example be the type declaration of an identifier, +/// or the type declaration of a structured member identifier. +/// +/// @returns the resolved type declaration if found, or nullptr otherwise. +frontend::Declaration const* referencedDeclaration(frontend::Expression const* _expression); + +/// @returns the location of the declaration's name, if present, or the location of the complete +/// declaration otherwise. If the input declaration is nullptr, std::nullopt is returned instead. +std::optional declarationLocation(frontend::Declaration const* _declaration); } diff --git a/test/libsolidity/lsp/goto_definition.sol b/test/libsolidity/lsp/goto_definition.sol new file mode 100644 index 000000000..675c1297c --- /dev/null +++ b/test/libsolidity/lsp/goto_definition.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import "./lib.sol"; + +interface I +{ + function f(uint x) external returns (uint); +} + +contract IA is I +{ + function f(uint x) public pure override returns (uint) { return x + 1; } +} + +contract IB is I +{ + function f(uint x) public pure override returns (uint) { return x + 2; } +} + +library IntLib +{ + function add(int self, int b) public pure returns (int) { return self + b; } +} + +contract C +{ + I obj; + function virtual_inheritance() public payable + { + obj = new IA(); + obj.f(1); // goto-definition should jump to definition of interface. + } + + using IntLib for *; + function using_for(int i) pure public + { + i.add(5); + 14.add(4); + } + + function useLib(uint n) public payable returns (uint) + { + return Lib.add(n, 1); + } + + function enums(Color c) public pure returns (Color d) + { + Color e = Color.Red; + if (c == e) + d = Color.Green; + else + d = c; + } + + type Price is uint128; + function udlTest() public pure returns (uint128) + { + Price p = Price.wrap(128); + return Price.unwrap(p); + } + + function structCtorTest(uint8 v) public pure returns (uint8 result) + { + RGBColor memory c = RGBColor(v, 2 * v, 3 * v); + result = c.red; + } +} diff --git a/test/libsolidity/lsp/goto_definition_imports.sol b/test/libsolidity/lsp/goto_definition_imports.sol new file mode 100644 index 000000000..b3df921fe --- /dev/null +++ b/test/libsolidity/lsp/goto_definition_imports.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.0; + +import {Weather as Wetter} from "./lib.sol"; +import "./lib.sol" as That; + +contract C +{ + function test_symbol_alias() public pure returns (Wetter result) + { + result = Wetter.Sunny; + } + + function test_library_alias() public pure returns (That.Color result) + { + That.Color color = That.Color.Red; + result = color; + } +} diff --git a/test/libsolidity/lsp/lib.sol b/test/libsolidity/lsp/lib.sol index f4fb51e77..22efe6ca2 100644 --- a/test/libsolidity/lsp/lib.sol +++ b/test/libsolidity/lsp/lib.sol @@ -1,6 +1,25 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.0; +/// Some Error type E. +error E(uint, uint); + +enum Weather { + Sunny, + Cloudy, + Rainy +} + +/// Some custom Color enum type holding 3 colors. +enum Color { + /// Red color. + Red, + /// Green color. + Green, + /// Blue color. + Blue +} + library Lib { function add(uint a, uint b) public pure returns (uint result) @@ -13,3 +32,10 @@ library Lib uint unused; } } + +struct RGBColor +{ + uint8 red; + uint8 green; + uint8 blue; +} diff --git a/test/lsp.py b/test/lsp.py index f03d9bc09..a20c28c3c 100755 --- a/test/lsp.py +++ b/test/lsp.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 - +# pragma pylint: disable=too-many-lines import argparse import fnmatch import json @@ -361,6 +361,52 @@ class SolidityLSPTestSuite: # {{{ }, "diagnostic: check range" ) + + def expect_location( + self, + obj: dict, + uri: str, + lineNo: int, + startEndColumns: Tuple[int, int] + ): + """ + obj is an JSON object containing two keys: + - 'uri': a string of the document URI + - 'range': the location range, two child objects 'start' and 'end', + each having a 'line' and 'character' integer value. + """ + [startColumn, endColumn] = startEndColumns + self.expect_equal(obj['uri'], uri) + self.expect_equal(obj['range']['start']['line'], lineNo) + self.expect_equal(obj['range']['start']['character'], startColumn) + self.expect_equal(obj['range']['end']['line'], lineNo) + self.expect_equal(obj['range']['end']['character'], endColumn) + + def expect_goto_definition_location( + self, + solc: JsonRpcProcess, + document_uri: str, + document_position: Tuple[int, int], + expected_uri: str, + expected_lineNo: int, + expected_startEndColumns: Tuple[int, int], + description: str + ): + response = solc.call_method( + 'textDocument/definition', + { + 'textDocument': { + 'uri': document_uri, + }, + 'position': { + 'line': document_position[0], + 'character': document_position[1] + } + } + ) + message = "Goto definition (" + description + ")" + self.expect_equal(len(response['result']), 1, message) + self.expect_location(response['result'][0], expected_uri, expected_lineNo, expected_startEndColumns) # }}} # {{{ actual tests @@ -434,7 +480,7 @@ class SolidityLSPTestSuite: # {{{ report = published_diagnostics[1] self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") - self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) + self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=31, startEndColumns=(8, 19)) 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. @@ -451,8 +497,8 @@ class SolidityLSPTestSuite: # {{{ [ { 'range': { - 'start': { 'line': 5, 'character': 0 }, - 'end': { 'line': 10, 'character': 0 } + 'start': { 'line': 24, 'character': 0 }, + 'end': { 'line': 29, 'character': 0 } }, 'text': "" # deleting function `add` } @@ -497,7 +543,7 @@ class SolidityLSPTestSuite: # {{{ report = published_diagnostics[1] self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") - self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) + self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=31, startEndColumns=(8, 19)) def test_textDocument_didChange_updates_diagnostics(self, solc: JsonRpcProcess) -> None: self.setup_lsp(solc) @@ -555,8 +601,8 @@ class SolidityLSPTestSuite: # {{{ { 'range': { - 'start': { 'line': 12, 'character': 1 }, - 'end': { 'line': 13, 'character': 1 } + 'start': { 'line': 31, 'character': 1 }, + 'end': { 'line': 32, 'character': 1 } }, 'text': "" } @@ -673,7 +719,7 @@ class SolidityLSPTestSuite: # {{{ reports = self.wait_for_diagnostics(solc, 2) self.expect_equal(len(reports), 2, '') self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") - self.expect_diagnostic(reports[1]['diagnostics'][0], 2072, 12, (8, 19)) # unused variable in lib.sol + self.expect_diagnostic(reports[1]['diagnostics'][0], 2072, 31, (8, 19)) # unused variable in lib.sol # Now close the file and expect the warning for lib.sol to be removed solc.send_message( @@ -744,6 +790,231 @@ class SolidityLSPTestSuite: # {{{ self.expect_equal(len(report3['diagnostics']), 1, "one diagnostic") self.expect_diagnostic(report3['diagnostics'][0], 4126, 6, (1, 23)) + def test_textDocument_definition(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + FILE_NAME = 'goto_definition' + FILE_URI = self.get_test_file_uri(FILE_NAME) + LIB_URI = self.get_test_file_uri('lib') + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': self.get_test_file_contents(FILE_NAME) + } + }) + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(published_diagnostics), 2, "publish diagnostics for 2 files") + self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0) + self.expect_equal(len(published_diagnostics[1]['diagnostics']), 1) + self.expect_diagnostic(published_diagnostics[1]['diagnostics'][0], 2072, 31, (8, 19)) # unused variable in lib.sol + + # import directive + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(3, 9), # symbol `"./lib.sol"` in `import "./lib.sol"` + expected_uri=LIB_URI, + expected_lineNo=0, + expected_startEndColumns=(0, 0), + description="import directive" + ) + + # type symbol to jump to type defs (error, contract, enum, ...) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(30, 19), # symbol `IA` in `new IA()` + expected_uri=FILE_URI, + expected_lineNo=10, + expected_startEndColumns=(9, 11), + description="type symbol to jump to definition" + ) + + # virtual function lookup? + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(31, 12), # symbol `f`, jumps to interface definition + expected_uri=FILE_URI, + expected_lineNo=7, + expected_startEndColumns=(13, 14), + description="virtual function lookup" + ) + + # using for + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(37, 10), # symbol `add` in `i.add(5)` + expected_uri=FILE_URI, + expected_lineNo=22, + expected_startEndColumns=(13, 16), + description="using for" + ) + + # library + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(43, 15), # symbol `Lib` in `Lib.add(n, 1)` + expected_uri=LIB_URI, + expected_lineNo=22, + expected_startEndColumns=(8, 11), + description="Library symbol from different file" + ) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(43, 19), # symbol `add` in `Lib.add(n, 1)` + expected_uri=LIB_URI, + expected_lineNo=24, + expected_startEndColumns=(13, 16), + description="Library member symbol from different file" + ) + + # enum type symbol and enum values + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(46, 19), # symbol `Color` in function signature's parameter + expected_uri=LIB_URI, + expected_lineNo=13, + expected_startEndColumns=(5, 10), + description="Enum type" + ) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(48, 24), # symbol `Red` in `Color.Red` + expected_uri=LIB_URI, + expected_lineNo=15, + expected_startEndColumns=(4, 7), + description="Enum value" + ) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(48, 24), # symbol `Red` in `Color.Red` + expected_uri=LIB_URI, + expected_lineNo=15, + expected_startEndColumns=(4, 7), + description="Enum value" + ) + + # local variable declarations + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(49, 17), # symbol `e` in `(c == e)` + expected_uri=FILE_URI, + expected_lineNo=48, + expected_startEndColumns=(14, 15), + description="local variable declaration" + ) + + # User defined type + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(58, 8), # symbol `Price` in `Price p ...` + expected_uri=FILE_URI, + expected_lineNo=55, + expected_startEndColumns=(9, 14), + description="User defined type on left hand side" + ) + + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(58, 18), # symbol `Price` in `Price.wrap()` expected_uri=FILE_URI, + expected_uri=FILE_URI, + expected_lineNo=55, + expected_startEndColumns=(9, 14), + description="User defined type on right hand side." + ) + + # struct constructor also properly jumps to the struct's declaration. + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(64, 33), # symbol `RGBColor` right hand side expression. + expected_uri=LIB_URI, + expected_lineNo=35, + expected_startEndColumns=(7, 15), + description="Struct constructor." + ) + + def test_textDocument_definition_imports(self, solc: JsonRpcProcess) -> None: + self.setup_lsp(solc) + FILE_NAME = 'goto_definition_imports' + FILE_URI = self.get_test_file_uri(FILE_NAME) + LIB_URI = self.get_test_file_uri('lib') + solc.send_message('textDocument/didOpen', { + 'textDocument': { + 'uri': FILE_URI, + 'languageId': 'Solidity', + 'version': 1, + 'text': self.get_test_file_contents(FILE_NAME) + } + }) + published_diagnostics = self.wait_for_diagnostics(solc, 2) + self.expect_equal(len(published_diagnostics), 2, "publish diagnostics for 2 files") + self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0) + self.expect_equal(len(published_diagnostics[1]['diagnostics']), 1) + self.expect_diagnostic(published_diagnostics[1]['diagnostics'][0], 2072, 31, (8, 19)) # unused variable in lib.sol + + # import directive: test symbol alias + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(3, 9), # in `Weather` of `import {Weather as Wetter} from "./lib.sol"` + expected_uri=LIB_URI, + expected_lineNo=6, + expected_startEndColumns=(5, 12), + description="goto definition of symbol in symbol alias import directive" + ) + + # import directive: test symbol alias + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(8, 55), # `Wetter` in return type declaration + expected_uri=LIB_URI, + expected_lineNo=6, + expected_startEndColumns=(5, 12), + description="goto definition of symbol in symbol alias import directive" + ) + + # That.Color tests with `That` being the aliased library to be imported. + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(13, 55), # `That` in return type declaration + expected_uri=LIB_URI, + expected_lineNo=13, + expected_startEndColumns=(5, 10), + description="goto definition of symbol in symbol alias import directive" + ) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(15, 8), + expected_uri=LIB_URI, + expected_lineNo=13, + expected_startEndColumns=(5, 10), + description="`That` in LHS variable assignment" + ) + self.expect_goto_definition_location( + solc=solc, + document_uri=FILE_URI, + document_position=(15, 27), + expected_uri=FILE_URI, + expected_lineNo=4, + expected_startEndColumns=(22, 26), + description="`That` in expression" + ) + def test_textDocument_didChange_empty_file(self, solc: JsonRpcProcess) -> None: """ Starts with an empty file and changes it to look like