Merge pull request #13103 from ethereum/lsp-rename

Lsp rename feature
This commit is contained in:
Mathias L. Baumann 2022-07-08 15:01:37 +02:00 committed by GitHub
commit 8d6b20f763
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1654 additions and 41 deletions

View File

@ -159,6 +159,8 @@ set(sources
lsp/FileRepository.h
lsp/GotoDefinition.cpp
lsp/GotoDefinition.h
lsp/RenameSymbol.cpp
lsp/RenameSymbol.h
lsp/HandlerBase.cpp
lsp/HandlerBase.h
lsp/LanguageServer.cpp

View File

@ -44,7 +44,6 @@ public:
/// Changes the source identified by the LSP client path _uri to _text.
void setSourceByUri(std::string const& _uri, std::string _text);
void addOrUpdateFile(boost::filesystem::path const& _path, frontend::SourceCode _source);
void setSourceUnits(StringMap _sources);
frontend::ReadCallback::Result readFile(std::string const& _kind, std::string const& _sourceUnitName);
frontend::ReadCallback::Callback reader()

View File

@ -45,8 +45,8 @@ public:
/// from the JSON-RPC parameters.
std::pair<std::string, langutil::LineColumn> extractSourceUnitNameAndLineColumn(Json::Value const& _params) const;
langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.charStreamProvider(); }
FileRepository const& fileRepository() const noexcept { return m_server.fileRepository(); }
langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_server.compilerStack(); }
FileRepository& fileRepository() const noexcept { return m_server.fileRepository(); }
Transport& client() const noexcept { return m_server.client(); }
protected:

View File

@ -26,6 +26,7 @@
// LSP feature implementations
#include <libsolidity/lsp/GotoDefinition.h>
#include <libsolidity/lsp/RenameSymbol.h>
#include <libsolidity/lsp/SemanticTokensBuilder.h>
#include <liblangutil/SourceReferenceExtractor.h>
@ -124,6 +125,7 @@ LanguageServer::LanguageServer(Transport& _transport):
{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _2)},
{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _2)},
{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _2)},
{"textDocument/rename", RenameSymbol(*this) },
{"textDocument/implementation", GotoDefinition(*this) },
{"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)},
{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _2)},
@ -314,6 +316,8 @@ void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args)
replyArgs["capabilities"]["semanticTokensProvider"]["legend"] = semanticTokensLegend();
replyArgs["capabilities"]["semanticTokensProvider"]["range"] = false;
replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true
replyArgs["capabilities"]["renameProvider"] = true;
m_client.reply(_id, move(replyArgs));
}
@ -432,6 +436,7 @@ void LanguageServer::handleTextDocumentDidClose(Json::Value const& _args)
compileAndUpdateDiagnostics();
}
ASTNode const* LanguageServer::astNodeAtSourceLocation(std::string const& _sourceUnitName, LineColumn const& _filePos)
{
if (m_compilerStack.state() < CompilerStack::AnalysisPerformed)

View File

@ -33,6 +33,7 @@
namespace solidity::lsp
{
class RenameSymbol;
enum class ErrorCode;
/**
@ -60,7 +61,7 @@ public:
FileRepository& fileRepository() noexcept { return m_fileRepository; }
Transport& client() noexcept { return m_client; }
frontend::ASTNode const* astNodeAtSourceLocation(std::string const& _sourceUnitName, langutil::LineColumn const& _filePos);
langutil::CharStreamProvider const& charStreamProvider() const noexcept { return m_compilerStack; }
frontend::CompilerStack const& compilerStack() const noexcept { return m_compilerStack; }
private:
/// Checks if the server is initialized (to be used by messages that need it to be initialized).
@ -72,6 +73,7 @@ private:
void handleTextDocumentDidOpen(Json::Value const& _args);
void handleTextDocumentDidChange(Json::Value const& _args);
void handleTextDocumentDidClose(Json::Value const& _args);
void handleRename(Json::Value const& _args);
void handleGotoDefinition(MessageID _id, Json::Value const& _args);
void semanticTokensFull(MessageID _id, Json::Value const& _args);

View File

@ -0,0 +1,316 @@
/*
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/RenameSymbol.h>
#include <libsolidity/lsp/Utils.h>
#include <libyul/AST.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;
namespace
{
CallableDeclaration const* extractCallableDeclaration(FunctionCall const& _functionCall)
{
if (
auto const* functionType = dynamic_cast<FunctionType const*>(_functionCall.expression().annotation().type);
functionType && functionType->hasDeclaration()
)
if (auto const* functionDefinition = dynamic_cast<FunctionDefinition const*>(&functionType->declaration()))
return functionDefinition;
return nullptr;
}
}
void RenameSymbol::operator()(MessageID _id, Json::Value const& _args)
{
auto const&& [sourceUnitName, lineColumn] = extractSourceUnitNameAndLineColumn(_args);
string const newName = _args["newName"].asString();
string const uri = _args["textDocument"]["uri"].asString();
ASTNode const* sourceNode = m_server.astNodeAtSourceLocation(sourceUnitName, lineColumn);
m_symbolName = {};
m_declarationToRename = nullptr;
m_sourceUnits = { &m_server.compilerStack().ast(sourceUnitName) };
m_locations.clear();
optional<int> cursorBytePosition = charStreamProvider()
.charStream(sourceUnitName)
.translateLineColumnToPosition(lineColumn);
solAssert(cursorBytePosition.has_value(), "Expected source pos");
extractNameAndDeclaration(*sourceNode, *cursorBytePosition);
// Find all source units using this symbol
for (auto const& [name, content]: fileRepository().sourceUnits())
{
auto const& sourceUnit = m_server.compilerStack().ast(name);
for (auto const* referencedSourceUnit: sourceUnit.referencedSourceUnits(true, util::convertContainer<set<SourceUnit const*>>(m_sourceUnits)))
if (*referencedSourceUnit->location().sourceName == sourceUnitName)
{
m_sourceUnits.insert(&sourceUnit);
break;
}
}
// Origin source unit should always be checked
m_sourceUnits.insert(&m_declarationToRename->sourceUnit());
Visitor visitor(*this);
for (auto const* sourceUnit: m_sourceUnits)
sourceUnit->accept(visitor);
// Apply changes in reverse order (will iterate in reverse)
sort(m_locations.begin(), m_locations.end());
Json::Value reply = Json::objectValue;
reply["changes"] = Json::objectValue;
Json::Value edits = Json::arrayValue;
for (auto i = m_locations.rbegin(); i != m_locations.rend(); i++)
{
solAssert(i->isValid());
// Replace in our file repository
string const uri = fileRepository().sourceUnitNameToUri(*i->sourceName);
string buffer = fileRepository().sourceUnits().at(*i->sourceName);
buffer.replace((size_t)i->start, (size_t)(i->end - i->start), newName);
fileRepository().setSourceByUri(uri, std::move(buffer));
Json::Value edit = Json::objectValue;
edit["range"] = toRange(*i);
edit["newText"] = newName;
// Record changes for the client
edits.append(edit);
if (i + 1 == m_locations.rend() || (i + 1)->sourceName != i->sourceName)
{
reply["changes"][uri] = edits;
edits = Json::arrayValue;
}
}
client().reply(_id, reply);
}
void RenameSymbol::extractNameAndDeclaration(ASTNode const& _node, int _cursorBytePosition)
{
// Identify symbol name and node
if (auto const* declaration = dynamic_cast<Declaration const*>(&_node))
{
if (declaration->nameLocation().containsOffset(_cursorBytePosition))
{
m_symbolName = declaration->name();
m_declarationToRename = declaration;
}
else if (auto const* importDirective = dynamic_cast<ImportDirective const*>(declaration))
extractNameAndDeclaration(*importDirective, _cursorBytePosition);
}
else if (auto const* identifier = dynamic_cast<Identifier const*>(&_node))
{
if (auto const* declReference = dynamic_cast<Declaration const*>(identifier->annotation().referencedDeclaration))
{
m_symbolName = identifier->name();
m_declarationToRename = declReference;
}
}
else if (auto const* identifierPath = dynamic_cast<IdentifierPath const*>(&_node))
extractNameAndDeclaration(*identifierPath, _cursorBytePosition);
else if (auto const* memberAccess = dynamic_cast<MemberAccess const*>(&_node))
{
m_symbolName = memberAccess->memberName();
m_declarationToRename = memberAccess->annotation().referencedDeclaration;
}
else if (auto const* functionCall = dynamic_cast<FunctionCall const*>(&_node))
extractNameAndDeclaration(*functionCall, _cursorBytePosition);
else if (auto const* inlineAssembly = dynamic_cast<InlineAssembly const*>(&_node))
extractNameAndDeclaration(*inlineAssembly, _cursorBytePosition);
else
solAssert(false, "Unexpected ASTNODE id: " + to_string(_node.id()));
lspDebug(fmt::format("Goal: rename '{}', loc: {}-{}", m_symbolName, m_declarationToRename->nameLocation().start, m_declarationToRename->nameLocation().end));
}
void RenameSymbol::extractNameAndDeclaration(ImportDirective const& _importDirective, int _cursorBytePosition)
{
for (ImportDirective::SymbolAlias const& symbolAlias: _importDirective.symbolAliases())
if (symbolAlias.location.containsOffset(_cursorBytePosition))
{
solAssert(symbolAlias.alias);
m_symbolName = *symbolAlias.alias;
m_declarationToRename = symbolAlias.symbol->annotation().referencedDeclaration;
break;
}
}
void RenameSymbol::Visitor::endVisit(ImportDirective const& _node)
{
// Handles SourceUnit aliases
if (handleGenericDeclaration(_node))
return;
for (ImportDirective::SymbolAlias const& symbolAlias: _node.symbolAliases())
if (
symbolAlias.alias != nullptr &&
*symbolAlias.alias == m_outer.m_symbolName &&
symbolAlias.symbol->annotation().referencedDeclaration == m_outer.m_declarationToRename
)
m_outer.m_locations.emplace_back(symbolAlias.location);
}
void RenameSymbol::extractNameAndDeclaration(FunctionCall const& _functionCall, int _cursorBytePosition)
{
if (auto const* functionDefinition = extractCallableDeclaration(_functionCall))
for (size_t i = 0; i < _functionCall.names().size(); i++)
if (_functionCall.nameLocations()[i].containsOffset(_cursorBytePosition))
{
m_symbolName = *_functionCall.names()[i];
for (size_t j = 0; j < functionDefinition->parameters().size(); j++)
if (
functionDefinition->parameters()[j] &&
functionDefinition->parameters()[j]->name() == m_symbolName
)
m_declarationToRename = functionDefinition->parameters()[j].get();
return;
}
}
void RenameSymbol::Visitor::endVisit(FunctionCall const& _node)
{
SourceLocation nameLocationInFunctionCall;
for (size_t i = 0; i < _node.names().size(); i++)
if (_node.names()[i] && *_node.names()[i] == m_outer.m_symbolName)
nameLocationInFunctionCall = _node.nameLocations()[i];
if (!nameLocationInFunctionCall.isValid())
return;
if (auto const* functionDefinition = extractCallableDeclaration(_node))
for (size_t j = 0; j < functionDefinition->parameters().size(); j++)
if (
functionDefinition->parameters()[j] &&
*functionDefinition->parameters()[j] == *m_outer.m_declarationToRename
)
m_outer.m_locations.emplace_back(nameLocationInFunctionCall);
}
void RenameSymbol::Visitor::endVisit(MemberAccess const& _node)
{
if (
m_outer.m_symbolName == _node.memberName() &&
*m_outer.m_declarationToRename == *_node.annotation().referencedDeclaration
)
m_outer.m_locations.emplace_back(_node.memberLocation());
}
void RenameSymbol::Visitor::endVisit(Identifier const& _node)
{
if (
m_outer.m_symbolName == _node.name() &&
*m_outer.m_declarationToRename == *_node.annotation().referencedDeclaration
)
m_outer.m_locations.emplace_back(_node.location());
}
void RenameSymbol::extractNameAndDeclaration(IdentifierPath const& _identifierPath, int _cursorBytePosition)
{
// iterate through the elements of the path to find the one the cursor is on
size_t numIdentifiers = _identifierPath.pathLocations().size();
for (size_t i = 0; i < numIdentifiers; i++)
{
auto& location = _identifierPath.pathLocations()[i];
if (location.containsOffset(_cursorBytePosition))
{
solAssert(_identifierPath.annotation().pathDeclarations.size() == numIdentifiers);
solAssert(_identifierPath.path().size() == numIdentifiers);
m_declarationToRename = _identifierPath.annotation().pathDeclarations[i];
m_symbolName = _identifierPath.path()[i];
}
}
}
void RenameSymbol::Visitor::endVisit(IdentifierPath const& _node)
{
std::vector<Declaration const*>& declarations = _node.annotation().pathDeclarations;
solAssert(declarations.size() == _node.path().size());
for (size_t i = 0; i < _node.path().size(); i++)
if (
_node.path()[i] == m_outer.m_symbolName &&
declarations[i] == m_outer.m_declarationToRename
)
m_outer.m_locations.emplace_back(_node.pathLocations()[i]);
}
void RenameSymbol::extractNameAndDeclaration(InlineAssembly const& _inlineAssembly, int _cursorBytePosition)
{
for (auto&& [identifier, externalReference]: _inlineAssembly.annotation().externalReferences)
{
SourceLocation location = yul::nativeLocationOf(*identifier);
location.end -= static_cast<int>(externalReference.suffix.size() + 1);
if (location.containsOffset(_cursorBytePosition))
{
m_declarationToRename = externalReference.declaration;
m_symbolName = identifier->name.str();
if (!externalReference.suffix.empty())
m_symbolName = m_symbolName.substr(0, m_symbolName.length() - externalReference.suffix.size() - 1);
break;
}
}
}
void RenameSymbol::Visitor::endVisit(InlineAssembly const& _node)
{
for (auto&& [identifier, externalReference]: _node.annotation().externalReferences)
{
string identifierName = identifier->name.str();
if (!externalReference.suffix.empty())
identifierName = identifierName.substr(0, identifierName.length() - externalReference.suffix.size() - 1);
if (
externalReference.declaration == m_outer.m_declarationToRename &&
identifierName == m_outer.m_symbolName
)
{
SourceLocation location = yul::nativeLocationOf(*identifier);
location.end -= static_cast<int>(externalReference.suffix.size() + 1);
m_outer.m_locations.emplace_back(location);
}
}
}

View File

@ -0,0 +1,119 @@
/*
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>
#include <libsolidity/ast/AST.h>
#include <libsolidity/ast/ASTVisitor.h>
namespace solidity::lsp
{
class RenameSymbol: public HandlerBase
{
public:
explicit RenameSymbol(LanguageServer& _server): HandlerBase(_server) {}
void operator()(MessageID, Json::Value const&);
protected:
// Nested class because otherwise `RenameSymbol` couldn't be easily used
// with LanguageServer::m_handlers as `ASTConstVisitor` deletes required
// c'tors
struct Visitor: public frontend::ASTConstVisitor
{
explicit Visitor(RenameSymbol& _outer): m_outer(_outer) {}
void endVisit(frontend::ImportDirective const& _node) override;
void endVisit(frontend::MemberAccess const& _node) override;
void endVisit(frontend::Identifier const& _node) override;
void endVisit(frontend::IdentifierPath const& _node) override;
void endVisit(frontend::FunctionCall const& _node) override;
void endVisit(frontend::InlineAssembly const& _node) override;
void endVisit(frontend::ContractDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::StructDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::EnumDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::EnumValue const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::UserDefinedValueTypeDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::VariableDeclaration const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::FunctionDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::ModifierDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::EventDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
void endVisit(frontend::ErrorDefinition const& _node) override
{
handleGenericDeclaration(_node);
}
bool handleGenericDeclaration(frontend::Declaration const& _declaration)
{
if (
m_outer.m_symbolName == _declaration.name() &&
*m_outer.m_declarationToRename == _declaration
)
{
m_outer.m_locations.emplace_back(_declaration.nameLocation());
return true;
}
return false;
}
private:
RenameSymbol& m_outer;
};
void extractNameAndDeclaration(frontend::ASTNode const& _node, int _cursorBytePosition);
void extractNameAndDeclaration(frontend::IdentifierPath const& _identifierPath, int _cursorBytePosition);
void extractNameAndDeclaration(frontend::ImportDirective const& _importDirective, int _cursorBytePosition);
void extractNameAndDeclaration(frontend::FunctionCall const& _functionCall, int _cursorBytePosition);
void extractNameAndDeclaration(frontend::InlineAssembly const& _inlineAssembly, int _cursorBytePosition);
// Node to rename
frontend::Declaration const* m_declarationToRename = nullptr;
// Original name
frontend::ASTString m_symbolName = {};
// SourceUnits to search & replace symbol in
std::set<frontend::SourceUnit const*, frontend::ASTNode::CompareByID> m_sourceUnits = {};
// Source locations that need to be replaced
std::vector<langutil::SourceLocation> m_locations = {};
};
}

View File

@ -195,7 +195,7 @@ void SemanticTokensBuilder::endVisit(frontend::StructuredDocumentation const& _d
void SemanticTokensBuilder::endVisit(frontend::Identifier const& _identifier)
{
lspDebug(fmt::format("Identifier: {}, {}..{} cat={}", _identifier.name(), _identifier.location().start, _identifier.location().end, _identifier.annotation().type->category()));
//lspDebug(fmt::format("Identifier: {}, {}..{} cat={}", _identifier.name(), _identifier.location().start, _identifier.location().end, _identifier.annotation().type->category()));
SemanticTokenModifiers modifiers = SemanticTokenModifiers::None;
if (_identifier.annotation().isConstant.set() && *_identifier.annotation().isConstant)

View File

@ -0,0 +1,307 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
contract ToRename
// ^ @CursorOnContractDefinition
// ^^^^^^^^ @ContractInDefinition
{
}
contract User
// ^^^^ @UserContractInContractTest
{
ToRename public publicVariable;
// ^^^^^^^^ @ContractInPublicVariable
// ^ @CursorOnPublicVariableType
ToRename[10] previousContracts;
// ^^^^^^^^ @ContractInArrayType
// ^ @CursorOnArrayType
mapping(int => ToRename) contractMapping;
// ^^^^^^^^ @ContractInMapping
// ^ @CursorOnMapping
function getContract() public returns (ToRename)
// ^^^^^^^^ @ContractInReturnParameter
// ^ @CursorOnReturnParameter
{
return new ToRename();
// ^^^^^^^^ @ContractInReturnExpression
// ^ @CursorOnReturnExpression
}
function setContract(ToRename _contract) public
// ^^^^^^^^ @ContractInParameter
// ^ @CursorOnParameter
{
publicVariable = _contract;
}
}
// ----
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnContractDefinition
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnReturnParameter
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnReturnExpression
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnPublicVariableType
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnArrayType
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnMapping
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnParameter
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ]
// }
// }

View File

@ -0,0 +1,194 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
contract C
{
function renameMe() public pure returns (int)
// ^^^^^^^^ @FunctionInDefinition
// ^ @CursorInDefinition
{
return 1;
}
function other() public view
{
renameMe();
// ^^^^^^^^ @FunctionInFunctionSameContract
// ^ @CursorInFunctionSameContract
this.renameMe();
// ^^^^^^^^ @FunctionInFunctionSameContractExternal
// ^ @CursorInFunctionSameContractExternal
}
}
contract Other
{
C m_contract;
function other() public view
{
m_contract.renameMe();
// ^^^^^^^^ @FunctionInFunctionOtherContract
// ^ @CursorInFunctionOtherContract
}
}
function free() pure
{
C local_contract;
local_contract.renameMe();
// ^^^^^^^^ @FunctionInFreeFunction
// ^ @CursorInFreeFunction
}
// ----
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorInDefinition
// }
// <- {
// "changes": {
// "rename/function.sol": [
// {
// "newText": "Renamed",
// "range": @FunctionInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionOtherContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContractExternal
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorInFunctionOtherContract
// }
// <- {
// "changes": {
// "rename/function.sol": [
// {
// "newText": "Renamed",
// "range": @FunctionInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionOtherContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContractExternal
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorInFunctionSameContractExternal
// }
// <- {
// "changes": {
// "rename/function.sol": [
// {
// "newText": "Renamed",
// "range": @FunctionInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionOtherContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContractExternal
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorInFunctionSameContract
// }
// <- {
// "changes": {
// "rename/function.sol": [
// {
// "newText": "Renamed",
// "range": @FunctionInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionOtherContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContractExternal
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorInFreeFunction
// }
// <- {
// "changes": {
// "rename/function.sol": [
// {
// "newText": "Renamed",
// "range": @FunctionInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionOtherContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContractExternal
// },
// {
// "newText": "Renamed",
// "range": @FunctionInFunctionSameContract
// },
// {
// "newText": "Renamed",
// "range": @FunctionInDefinition
// }
// ]
// }
// }

View File

@ -0,0 +1,202 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
contract C
{
function foo(int a, int b, int c) pure public returns(int)
// ^ @ParameterB
// ^ @ParameterA
// ^ @ParameterC
{
return a + b + c;
// ^ @ParameterBInFoo
// ^ @ParameterAInFoo
// ^ @ParameterCInFoo
}
function bar() public view
{
this.foo({c:1, b:2, a:3});
// ^ @ParameterBInCall
// ^ @ParameterCInCall
// ^ @ParameterAInCall
}
}
// ----
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterA
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterAInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterAInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterA
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterAInCall
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterAInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterAInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterA
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterAInFoo
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterAInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterAInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterA
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterC
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterCInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterCInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterC
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterCInCall
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterCInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterCInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterC
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterCInFoo
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterCInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterCInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterC
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterBInCall
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterBInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterBInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterB
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @ParameterBInFoo
// }
// <- {
// "changes": {
// "rename/functionCall.sol": [
// {
// "newText": "Renamed",
// "range": @ParameterBInCall
// },
// {
// "newText": "Renamed",
// "range": @ParameterBInFoo
// },
// {
// "newText": "Renamed",
// "range": @ParameterB
// }
// ]
// }
// }

View File

@ -0,0 +1,247 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
import "./contract.sol" as externalFile;
// ^^^^^^^^^^^^ @FileAliasInImportDirective
// ^ @CursorOnFileAliasInImportDirective
import {ToRename as ExternalContract, User} from "./contract.sol";
// ^^^^^^^^^^^^^^^^ @RenamedContractInImportDirective
// ^ @CursorOnRenamedContractInImportDirective
// ^^^^^^^^ @OriginalNameInImportDirective
// ^ @CursorOnOriginalNameInImportDirective
// ^^^^ @UserInImportDirective
// ^ @CursorOnUserInImportDirective
contract C
{
ExternalContract public externalContract;
// ^^^^^^^^^^^^^^^^ @RenamedContractInPublicVariable
// ^ @CursorOnRenamedContractInPublicVariable
externalFile.ToRename public externalFileContract;
// ^^^^^^^^^^^^ @FileAliasInPublicVariable
// ^ @CursorOnFileAliasInPublicVariable
// ^^^^^^^^ @OriginalNameInPublicVariable
// ^ @CursorOnOriginalNameInPublicVariable
User public externalUserContract;
// ^^^^ @UserInPublicVariable
// ^ @CursorOnUserInPublicVariable
}
// ----
// contract:
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnFileAliasInImportDirective
// }
// <- {
// "changes": {
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @FileAliasInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @FileAliasInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnRenamedContractInImportDirective
// }
// <- {
// "changes": {
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @RenamedContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @RenamedContractInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnOriginalNameInImportDirective
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ],
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @OriginalNameInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @OriginalNameInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnRenamedContractInPublicVariable
// }
// <- {
// "changes": {
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @RenamedContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @RenamedContractInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnFileAliasInPublicVariable
// }
// <- {
// "changes": {
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @FileAliasInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @FileAliasInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnOriginalNameInPublicVariable
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @ContractInParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnExpression
// },
// {
// "newText": "Renamed",
// "range": @ContractInReturnParameter
// },
// {
// "newText": "Renamed",
// "range": @ContractInMapping
// },
// {
// "newText": "Renamed",
// "range": @ContractInArrayType
// },
// {
// "newText": "Renamed",
// "range": @ContractInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @ContractInDefinition
// }
// ],
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @OriginalNameInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @OriginalNameInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnUserInPublicVariable
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @UserContractInContractTest
// }
// ],
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @UserInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @UserInImportDirective
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnUserInImportDirective
// }
// <- {
// "changes": {
// "rename/contract.sol": [
// {
// "newText": "Renamed",
// "range": @UserContractInContractTest
// }
// ],
// "rename/import_directive.sol": [
// {
// "newText": "Renamed",
// "range": @UserInPublicVariable
// },
// {
// "newText": "Renamed",
// "range": @UserInImportDirective
// }
// ]
// }
// }

View File

@ -0,0 +1,132 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.0;
contract C
{
int public renameMe;
// ^^^^^^^^ @VariableInDefinition
// ^ @CursorOnVariableDefinition
function foo() public returns(int)
{
renameMe = 1;
// ^^^^^^^^ @VariableInFunction
// ^ @CursorOnVariableInFunction
return this.renameMe();
// ^^^^^^^^ @VariableInGetter
// ^ @CursorOnVariableInGetter
}
}
function freeFunction(C _contract) view returns(int)
{
return _contract.renameMe();
// ^^^^^^^^ @VariableInFreeFunction
// ^ @CursorOnVariableInFreeFunction
}
// ----
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnVariableInFunction
// }
// <- {
// "changes": {
// "rename/variable.sol": [
// {
// "newText": "Renamed",
// "range": @VariableInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInGetter
// },
// {
// "newText": "Renamed",
// "range": @VariableInFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnVariableDefinition
// }
// <- {
// "changes": {
// "rename/variable.sol": [
// {
// "newText": "Renamed",
// "range": @VariableInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInGetter
// },
// {
// "newText": "Renamed",
// "range": @VariableInFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnVariableInGetter
// }
// <- {
// "changes": {
// "rename/variable.sol": [
// {
// "newText": "Renamed",
// "range": @VariableInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInGetter
// },
// {
// "newText": "Renamed",
// "range": @VariableInFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInDefinition
// }
// ]
// }
// }
// -> textDocument/rename {
// "newName": "Renamed",
// "position": @CursorOnVariableInFreeFunction
// }
// <- {
// "changes": {
// "rename/variable.sol": [
// {
// "newText": "Renamed",
// "range": @VariableInFreeFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInGetter
// },
// {
// "newText": "Renamed",
// "range": @VariableInFunction
// },
// {
// "newText": "Renamed",
// "range": @VariableInDefinition
// }
// ]
// }
// }

View File

@ -15,7 +15,7 @@ from copy import deepcopy
from enum import Enum, auto
from itertools import islice
from pathlib import PurePath
from typing import Any, List, Optional, Tuple, Union
from typing import Any, List, Optional, Tuple, Union, NewType
import colorama # Enables the use of SGR & CUP terminal VT sequences on Windows.
from deepdiff import DeepDiff
@ -30,6 +30,13 @@ else:
tty.setcbreak(sys.stdin.fileno())
# Type for the pure test name without .sol suffix or sub directory
TestName = NewType("TestName", str)
# Type for the test path, e.g. subdir/mytest.sol
RelativeTestPath = NewType("RelativeTestPath", str)
def escape_string(text: str) -> str:
"""
Trivially escapes given input string's \r \n and \\.
@ -148,11 +155,13 @@ class JsonRpcProcess:
exe_args: List[str]
process: subprocess.Popen
trace_io: bool
print_pid: bool
def __init__(self, exe_path: str, exe_args: List[str], trace_io: bool = True):
def __init__(self, exe_path: str, exe_args: List[str], trace_io: bool = True, print_pid = False):
self.exe_path = exe_path
self.exe_args = exe_args
self.trace_io = trace_io
self.print_pid = print_pid
def __enter__(self):
self.process = subprocess.Popen(
@ -161,6 +170,10 @@ class JsonRpcProcess:
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if self.print_pid:
print(f"solc pid: {self.process.pid}. Attach with sudo gdb -p {self.process.pid}")
return self
def __exit__(self, exception_type, exception_value, traceback) -> None:
@ -285,6 +298,13 @@ def create_cli_parser() -> argparse.ArgumentParser:
action="store_true",
help="Prevent interactive queries and just fail instead."
)
parser.set_defaults(print_solc_pid=False)
parser.add_argument(
"-p", "--print-solc-pid",
dest="print_solc_pid",
action="store_true",
help="Print pid of each started solc for debugging purposes."
)
parser.set_defaults(trace_io=False)
parser.add_argument(
"-T", "--trace-io",
@ -347,10 +367,13 @@ class TestParser:
parsed_testcases = TestParser(content).parse()
# First diagnostics are yielded
# First diagnostics are yielded.
# Type is "TestParser.Diagnostics"
expected_diagnostics = next(parsed_testcases)
...
# Now each request/response pair in the test definition
# Type is "TestParser.RequestAndResponse"
for testcase in self.parsed_testcases:
...
"""
@ -393,11 +416,11 @@ class TestParser:
yield self.parseDiagnostics()
while not self.at_end():
yield self.RequestAndResponse(**self.parseRequestAndResponse())
yield self.parseRequestAndResponse()
self.next_line()
def parseDiagnostics(self):
def parseDiagnostics(self) -> Diagnostics:
"""
Parse diagnostic expectations specified in the file.
Returns a named tuple instance of "Diagnostics"
@ -429,7 +452,7 @@ class TestParser:
return self.Diagnostics(**diagnostics)
def parseRequestAndResponse(self):
def parseRequestAndResponse(self) -> RequestAndResponse:
RESPONSE_START = "// <- "
REQUEST_END = "// }"
COMMENT_PREFIX = "// "
@ -490,7 +513,7 @@ class TestParser:
if self.at_end():
raise TestParserException(ret, "Response footer not found")
return ret
return self.RequestAndResponse(**ret)
def next_line(self):
self.current_line_tuple = next(self.lines, None)
@ -532,7 +555,7 @@ class FileTestRunner:
self.solc = solc
self.open_tests = []
self.content = self.suite.get_test_file_contents(self.test_name, self.sub_dir)
self.markers = self.suite.get_file_tags(self.test_name, self.sub_dir)
self.markers = self.suite.get_test_tags(self.test_name, self.sub_dir)
self.parsed_testcases = None
self.expected_diagnostics = None
@ -580,7 +603,7 @@ class FileTestRunner:
len(expected_diagnostics),
description="Unexpected amount of diagnostics"
)
markers = self.suite.get_file_tags(testname, sub_dir)
markers = self.suite.get_test_tags(testname, sub_dir)
for actual_diagnostic in diagnostics_per_file["diagnostics"]:
expected_diagnostic = next((diagnostic for diagnostic in
expected_diagnostics if actual_diagnostic['range'] ==
@ -643,7 +666,13 @@ class FileTestRunner:
finally:
self.close_all_open_files()
def user_interaction_failed_method_test(self, testcase, actual, expected) -> TestResult:
def user_interaction_failed_method_test(
self,
testcase: TestParser.RequestAndResponse,
actual,
expected
) -> TestResult:
actual_pretty = self.suite.replace_ranges_with_tags(actual, self.sub_dir)
if expected is None:
@ -688,16 +717,29 @@ class FileTestRunner:
"""
Runs the given testcase.
"""
requestBodyJson = self.parse_json_with_tags(testcase.request, self.markers)
# add textDocument/uri if missing
if 'textDocument' not in requestBodyJson:
requestBodyJson['textDocument'] = { 'uri': self.suite.get_test_file_uri(self.test_name, self.sub_dir) }
actualResponseJson = self.solc.call_method(testcase.method, requestBodyJson)
# simplify response
for result in actualResponseJson["result"]:
if "uri" in result:
result["uri"] = result["uri"].replace(self.suite.project_root_uri + "/" + self.sub_dir + "/", "")
if "result" in actualResponseJson:
if isinstance(actualResponseJson["result"], list):
for result in actualResponseJson["result"]:
if "uri" in result:
result["uri"] = result["uri"].replace(self.suite.project_root_uri + "/" + self.sub_dir + "/", "")
elif isinstance(actualResponseJson["result"], dict):
if "changes" in actualResponseJson["result"]:
changes = actualResponseJson["result"]["changes"]
for key in list(changes.keys()):
new_key = key.replace(self.suite.project_root_uri + "/", "")
changes[new_key] = changes[key]
del changes[key]
if "jsonrpc" in actualResponseJson:
actualResponseJson.pop("jsonrpc")
@ -737,21 +779,39 @@ class FileTestRunner:
if not isinstance(data, dict):
return data
def findMarker(desired_tag):
if not isinstance(desired_tag, str):
return desired_tag
for tag, tagRange in markers.items():
if tag == desired_tag:
return tagRange
elif tag.lower() == desired_tag.lower():
raise Exception(f"Detected lower/upper case mismatch: Requested {desired_tag} but only found {tag}")
raise Exception(f"Marker {desired_tag} not found in file")
# Check if we need markers from a specific file
# Needs to be done before the loop or it might be called only after
# we found "range" or "position"
if "uri" in data:
markers = self.suite.get_file_tags(data["uri"][:-len(".sol")], self.sub_dir)
markers = self.suite.get_test_tags(data["uri"][:-len(".sol")], self.sub_dir)
for key, val in data.items():
if key == "range":
for tag, tagRange in markers.items():
if tag == val:
data[key] = tagRange
data[key] = findMarker(val)
elif key == "position":
for tag, tagRange in markers.items():
if tag == val:
data[key] = tagRange["start"]
tag_range = findMarker(val)
if "start" in tag_range:
data[key] = tag_range["start"]
elif key == "changes":
for path, list_of_changes in val.items():
test_name, file_sub_dir = split_path(path)
markers = self.suite.get_test_tags(test_name[:-len(".sol")], file_sub_dir)
for change in list_of_changes:
if "range" in change:
change["range"] = findMarker(change["range"])
elif isinstance(val, dict):
replace_tag(val, markers)
elif isinstance(val, list):
@ -781,6 +841,7 @@ class SolidityLSPTestSuite: # {{{
self.test_pattern = args.test_pattern
self.fail_fast = args.fail_fast
self.non_interactive = args.non_interactive
self.print_solc_pid = args.print_solc_pid
print(f"{SGR_NOTICE}test pattern: {self.test_pattern}{SGR_RESET}")
@ -803,7 +864,7 @@ class SolidityLSPTestSuite: # {{{
title: str = test_fn.__name__[5:]
print(f"{SGR_TEST_BEGIN}Testing {title} ...{SGR_RESET}")
try:
with JsonRpcProcess(self.solc_path, ["--lsp"], trace_io=self.trace_io) as solc:
with JsonRpcProcess(self.solc_path, ["--lsp"], trace_io=self.trace_io, print_pid=self.print_solc_pid) as solc:
test_fn(solc)
self.test_counter.passed += 1
except ExpectationFailed:
@ -1102,7 +1163,7 @@ class SolidityLSPTestSuite: # {{{
Find and return the tag that represents the requested range otherwise
return None.
"""
markers = self.get_file_tags(test, sub_dir)
markers = self.get_test_tags(test, sub_dir)
for tag, tag_range in markers.items():
if tag_range == target_range:
@ -1113,8 +1174,18 @@ class SolidityLSPTestSuite: # {{{
def replace_ranges_with_tags(self, content, sub_dir):
"""
Replace matching ranges with "@<tagname>".
Recognized patterns:
{ "changes": { "<uri>": { "range": "<range>" } } }
{ "uri": "<uri>", "range": "<range> }
"""
def replace_range(item: dict, markers):
for tag, tagRange in markers.items():
if "range" in item and tagRange == item["range"]:
item["range"] = str(tag)
def recursive_iter(obj):
if isinstance(obj, dict):
yield obj
@ -1126,10 +1197,27 @@ class SolidityLSPTestSuite: # {{{
for item in recursive_iter(content):
if "uri" in item and "range" in item:
markers = self.get_file_tags(item["uri"][:-len(".sol")], sub_dir)
for tag, tagRange in markers.items():
if tagRange == item["range"]:
item["range"] = str(tag)
try:
markers = self.get_test_tags(item["uri"][:-len(".sol")], sub_dir)
replace_range(item, markers)
except FileNotFoundError:
# Skip over errors as this is user provided input that can
# point to non-existing files
pass
elif "changes" in item:
for file, changes_for_file in item["changes"].items():
test_name, file_sub_dir = split_path(file)
try:
markers = self.get_test_tags(test_name[:-len(".sol")], file_sub_dir)
for change in changes_for_file:
replace_range(change, markers)
except FileNotFoundError:
# Skip over errors as this is user provided input that can
# point to non-existing files
pass
# Convert JSON to string and split it at the quoted tags
split_by_tag = TEST_REGEXES.findQuotedTag.split(json.dumps(content, indent=4, sort_keys=True))
@ -1178,7 +1266,7 @@ class SolidityLSPTestSuite: # {{{
if user_response == "r":
print("retrying...")
# pragma pylint: disable=no-member
self.get_file_tags.cache_clear()
self.get_test_tags.cache_clear()
return False
if user_response == "e":
editor = os.environ.get('VISUAL', os.environ.get('EDITOR', 'vi'))
@ -1188,7 +1276,7 @@ class SolidityLSPTestSuite: # {{{
check=True
)
# pragma pylint: disable=no-member
self.get_file_tags.cache_clear()
self.get_test_tags.cache_clear()
elif user_response == "s":
print("skipping...")
@ -1236,11 +1324,11 @@ class SolidityLSPTestSuite: # {{{
report = published_diagnostics[1]
self.expect_equal(report['uri'], self.get_test_file_uri('lib', 'goto'), "Correct file URI")
self.expect_equal(len(report['diagnostics']), 1, "one diagnostic")
marker = self.get_file_tags("lib", "goto")["@diagnostics"]
marker = self.get_test_tags("lib", "goto")["@diagnostics"]
self.expect_diagnostic(report['diagnostics'][0], code=2072, marker=marker)
@functools.lru_cache() # pragma pylint: disable=lru-cache-decorating-method
def get_file_tags(self, test_name: str, sub_dir=None, verbose=False):
def get_test_tags(self, test_name: TestName, sub_dir=None, verbose=False):
"""
Finds all tags (e.g. @tagname) in the given test and returns them as a
dictionary having the following structure: {
@ -1285,7 +1373,7 @@ class SolidityLSPTestSuite: # {{{
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.
self.test_textDocument_didOpen_with_relative_import(solc)
marker = self.get_file_tags("lib", "goto")["@addFunction"]
marker = self.get_test_tags("lib", "goto")["@addFunction"]
self.open_file_and_wait_for_diagnostics(solc, 'lib', "goto")
solc.send_message(
'textDocument/didChange',
@ -1310,7 +1398,7 @@ class SolidityLSPTestSuite: # {{{
report = published_diagnostics[0]
self.expect_equal(report['uri'], self.get_test_file_uri('didOpen_with_import'))
diagnostics = report['diagnostics']
marker = self.get_file_tags("didOpen_with_import")["@diagnostics"]
marker = self.get_test_tags("didOpen_with_import")["@diagnostics"]
self.expect_equal(len(diagnostics), 1, "now, no diagnostics")
self.expect_diagnostic(diagnostics[0], code=9582, marker=marker)
@ -1343,7 +1431,7 @@ class SolidityLSPTestSuite: # {{{
self.expect_equal(report['uri'], self.get_test_file_uri('lib', 'goto'), "Correct file URI")
self.expect_equal(len(report['diagnostics']), 1, "one diagnostic")
markers = self.get_file_tags('lib', 'goto')
markers = self.get_test_tags('lib', 'goto')
marker = markers["@diagnostics"]
self.expect_diagnostic(report['diagnostics'][0], code=2072, marker=marker)
@ -1400,7 +1488,7 @@ class SolidityLSPTestSuite: # {{{
self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME, "goto"), "Correct file URI")
diagnostics = report['diagnostics']
self.expect_equal(len(diagnostics), 3, "3 diagnostic messages")
markers = self.get_file_tags(TEST_NAME, "goto")
markers = self.get_test_tags(TEST_NAME, "goto")
self.expect_diagnostic(diagnostics[0], code=6321, marker=markers["@unusedReturnVariable"])
self.expect_diagnostic(diagnostics[1], code=2072, marker=markers["@unusedVariable"])
self.expect_diagnostic(diagnostics[2], code=2072, marker=markers["@unusedContractVariable"])
@ -1433,7 +1521,7 @@ class SolidityLSPTestSuite: # {{{
self.test_textDocument_didOpen_with_relative_import(solc)
self.open_file_and_wait_for_diagnostics(solc, 'lib', 'goto')
marker = self.get_file_tags('lib', 'goto')["@diagnostics"]
marker = self.get_test_tags('lib', 'goto')["@diagnostics"]
# lib.sol: Fix the unused variable message by removing it.
solc.send_message(
@ -1563,7 +1651,7 @@ class SolidityLSPTestSuite: # {{{
self.expect_equal(len(reports), 2, '')
self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics")
marker = self.get_file_tags("lib", 'goto')["@diagnostics"]
marker = self.get_test_tags("lib", 'goto')["@diagnostics"]
# unused variable in lib.sol
self.expect_diagnostic(reports[1]['diagnostics'][0], code=2072, marker=marker)