LSP: Implements goto-definition.

This commit is contained in:
Christian Parpart 2021-12-20 14:10:17 +01:00
parent 1035eacb53
commit 2b2f8acc12
16 changed files with 719 additions and 91 deletions

View File

@ -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(...)``.

View File

@ -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

View File

@ -18,12 +18,35 @@
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTUtils.h>
#include <libsolidity/ast/ASTVisitor.h>
#include <libsolutil/Algorithms.h>
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");

View File

@ -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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/GotoDefinition.h>
#include <libsolidity/lsp/Transport.h> // for RequestError
#include <libsolidity/lsp/Utils.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTUtils.h>
#include <fmt/format.h>
#include <memory>
#include <string>
#include <vector>
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<SourceLocation> locations;
if (auto const* expression = dynamic_cast<Expression const*>(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<IdentifierPath const*>(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<ImportDirective const*>(sourceNode))
{
auto const& path = *importDirective->annotation().absolutePath;
if (fileRepository().sourceUnits().count(path))
locations.emplace_back(SourceLocation{0, 0, make_shared<string const>(path)});
}
Json::Value reply = Json::arrayValue;
for (SourceLocation const& location: locations)
reply.append(toJson(location));
client().reply(_id, reply);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolidity/lsp/HandlerBase.h>
namespace solidity::lsp
{
class GotoDefinition: public HandlerBase
{
public:
explicit GotoDefinition(LanguageServer& _server): HandlerBase(_server) {}
void operator()(MessageID, Json::Value const&);
};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <libsolutil/Exceptions.h>
#include <libsolidity/lsp/HandlerBase.h>
#include <libsolidity/lsp/LanguageServer.h>
#include <libsolidity/lsp/Utils.h>
@ -5,13 +24,13 @@
#include <liblangutil/Exceptions.h>
#include <fmt/format.h>
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<SourceLocation> HandlerBase::parsePosition(string const& _sourceUnitName, Json::Value const& _position) const
pair<string, LineColumn> 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> lineColumn = parseLineColumn(_position))
if (optional<int> const offset = CharStream::translateLineColumnToPosition(
fileRepository().sourceUnits().at(_sourceUnitName),
*lineColumn
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 SourceLocation{*offset, *offset, make_shared<string>(_sourceUnitName)};
return nullopt;
}
optional<SourceLocation> HandlerBase::parseRange(string const& _sourceUnitName, Json::Value const& _range) const
{
if (!_range.isObject())
return nullopt;
optional<SourceLocation> start = parsePosition(_sourceUnitName, _range["start"]);
optional<SourceLocation> end = parsePosition(_sourceUnitName, _range["end"]);
if (!start || !end)
return nullopt;
solAssert(*start->sourceName == *end->sourceName);
start->end = end->end;
return start;
}
);
return {sourceUnitName, *lineColumn};
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <libsolidity/lsp/FileRepository.h>
@ -24,22 +41,15 @@ public:
Json::Value toRange(langutil::SourceLocation const& _location) const;
Json::Value toJson(langutil::SourceLocation const& _location) const;
std::optional<langutil::SourceLocation> 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<std::string, langutil::LineColumn> 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<langutil::SourceLocation> 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;
};

View File

@ -24,6 +24,8 @@
#include <libsolidity/lsp/HandlerBase.h>
#include <libsolidity/lsp/Utils.h>
// LSP feature implementations
#include <libsolidity/lsp/GotoDefinition.h>
#include <liblangutil/SourceReferenceExtractor.h>
#include <liblangutil/CharStream.h>
@ -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<SourceLocation> 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<SourceLocation> change = parseRange(sourceUnitName, jsonContentChange["range"]);
optional<SourceLocation> 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<int> sourcePos = m_compilerStack.charStream(_sourceUnitName)
.translateLineColumnToPosition(_filePos);
if (!sourcePos.has_value())
return nullptr;
if (optional<int> sourcePos =
m_compilerStack.charStream(_sourceUnitName).translateLineColumnToPosition(_filePos))
return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(_sourceUnitName));
else
return nullptr;
}

View File

@ -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<void(MessageID, Json::Value const&)>;
/// @returns the source location given a source unit name and an LSP Range object,
/// or nullopt on failure.
std::optional<langutil::SourceLocation> parseRange(
std::string const& _sourceUnitName,
Json::Value const& _range
);
Json::Value toRange(langutil::SourceLocation const& _location);
Json::Value toJson(langutil::SourceLocation const& _location);

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#include <liblangutil/CharStreamProvider.h>
#include <liblangutil/Exceptions.h>
#include <libsolidity/ast/AST.h>
#include <libsolidity/lsp/FileRepository.h>
#include <libsolidity/lsp/Utils.h>
#include <fmt/format.h>
#include <fstream>
namespace solidity::lsp
{
@ -19,7 +40,7 @@ optional<LineColumn> 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<Declaration const*> allAnnotatedDeclarations(Expression const* _expression)
Declaration const* referencedDeclaration(Expression const* _expression)
{
vector<Declaration const*> output;
if (auto const* identifier = dynamic_cast<Identifier const*>(_expression))
{
output.push_back(identifier->annotation().referencedDeclaration);
output += identifier->annotation().candidateDeclarations;
}
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(_expression))
{
output.push_back(memberAccess->annotation().referencedDeclaration);
if (Declaration const* referencedDeclaration = identifier->annotation().referencedDeclaration)
return referencedDeclaration;
if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(_expression))
if (memberAccess->annotation().referencedDeclaration)
return memberAccess->annotation().referencedDeclaration;
return nullptr;
}
return output;
}
optional<SourceLocation> declarationPosition(Declaration const* _declaration)
optional<SourceLocation> declarationLocation(Declaration const* _declaration)
{
if (!_declaration)
return nullopt;
@ -67,4 +84,35 @@ optional<SourceLocation> declarationPosition(Declaration const* _declaration)
return nullopt;
}
optional<SourceLocation> parsePosition(
FileRepository const& _fileRepository,
string const& _sourceUnitName,
Json::Value const& _position
)
{
if (!_fileRepository.sourceUnits().count(_sourceUnitName))
return nullopt;
if (optional<LineColumn> lineColumn = parseLineColumn(_position))
if (optional<int> const offset = CharStream::translateLineColumnToPosition(
_fileRepository.sourceUnits().at(_sourceUnitName),
*lineColumn
))
return SourceLocation{*offset, *offset, make_shared<string>(_sourceUnitName)};
return nullopt;
}
optional<SourceLocation> parseRange(FileRepository const& _fileRepository, string const& _sourceUnitName, Json::Value const& _range)
{
if (!_range.isObject())
return nullopt;
optional<SourceLocation> start = parsePosition(_fileRepository, _sourceUnitName, _range["start"]);
optional<SourceLocation> end = parsePosition(_fileRepository, _sourceUnitName, _range["end"]);
if (!start || !end)
return nullopt;
solAssert(*start->sourceName == *end->sourceName);
start->end = end->end;
return start;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
// SPDX-License-Identifier: GPL-3.0
#pragma once
#include <liblangutil/SourceLocation.h>
@ -9,6 +27,13 @@
#include <optional>
#include <vector>
#if !defined(NDEBUG)
#include <fstream>
#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<langutil::LineColumn> 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<frontend::Declaration const*> allAnnotatedDeclarations(frontend::Expression const* _expression);
std::optional<langutil::SourceLocation> 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<langutil::SourceLocation> 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<langutil::SourceLocation> 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<langutil::SourceLocation> declarationLocation(frontend::Declaration const* _declaration);
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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