diff --git a/libsolidity/CMakeLists.txt b/libsolidity/CMakeLists.txt index b609f4fee..68e58e1c9 100644 --- a/libsolidity/CMakeLists.txt +++ b/libsolidity/CMakeLists.txt @@ -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 diff --git a/libsolidity/lsp/FileRepository.h b/libsolidity/lsp/FileRepository.h index 152c1d4be..52e50b393 100644 --- a/libsolidity/lsp/FileRepository.h +++ b/libsolidity/lsp/FileRepository.h @@ -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() diff --git a/libsolidity/lsp/HandlerBase.h b/libsolidity/lsp/HandlerBase.h index 3ca1679be..e2ccb88d8 100644 --- a/libsolidity/lsp/HandlerBase.h +++ b/libsolidity/lsp/HandlerBase.h @@ -45,8 +45,8 @@ public: /// from the JSON-RPC parameters. std::pair 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: diff --git a/libsolidity/lsp/LanguageServer.cpp b/libsolidity/lsp/LanguageServer.cpp index ed1e5a6e1..685e0c631 100644 --- a/libsolidity/lsp/LanguageServer.cpp +++ b/libsolidity/lsp/LanguageServer.cpp @@ -26,6 +26,7 @@ // LSP feature implementations #include +#include #include #include @@ -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) diff --git a/libsolidity/lsp/LanguageServer.h b/libsolidity/lsp/LanguageServer.h index 4a683e290..ee4f06957 100644 --- a/libsolidity/lsp/LanguageServer.h +++ b/libsolidity/lsp/LanguageServer.h @@ -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); diff --git a/libsolidity/lsp/RenameSymbol.cpp b/libsolidity/lsp/RenameSymbol.cpp new file mode 100644 index 000000000..e205beb73 --- /dev/null +++ b/libsolidity/lsp/RenameSymbol.cpp @@ -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 . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include +#include + +#include + +#include + +#include +#include +#include + +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(_functionCall.expression().annotation().type); + functionType && functionType->hasDeclaration() + ) + if (auto const* functionDefinition = dynamic_cast(&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 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>(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(&_node)) + { + if (declaration->nameLocation().containsOffset(_cursorBytePosition)) + { + m_symbolName = declaration->name(); + m_declarationToRename = declaration; + } + else if (auto const* importDirective = dynamic_cast(declaration)) + extractNameAndDeclaration(*importDirective, _cursorBytePosition); + } + else if (auto const* identifier = dynamic_cast(&_node)) + { + if (auto const* declReference = dynamic_cast(identifier->annotation().referencedDeclaration)) + { + m_symbolName = identifier->name(); + m_declarationToRename = declReference; + } + } + else if (auto const* identifierPath = dynamic_cast(&_node)) + extractNameAndDeclaration(*identifierPath, _cursorBytePosition); + else if (auto const* memberAccess = dynamic_cast(&_node)) + { + m_symbolName = memberAccess->memberName(); + m_declarationToRename = memberAccess->annotation().referencedDeclaration; + } + else if (auto const* functionCall = dynamic_cast(&_node)) + extractNameAndDeclaration(*functionCall, _cursorBytePosition); + else if (auto const* inlineAssembly = dynamic_cast(&_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& 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(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(externalReference.suffix.size() + 1); + + m_outer.m_locations.emplace_back(location); + } + } + +} diff --git a/libsolidity/lsp/RenameSymbol.h b/libsolidity/lsp/RenameSymbol.h new file mode 100644 index 000000000..cafa8c067 --- /dev/null +++ b/libsolidity/lsp/RenameSymbol.h @@ -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 . +*/ +// SPDX-License-Identifier: GPL-3.0 +#include +#include +#include + +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 m_sourceUnits = {}; + // Source locations that need to be replaced + std::vector m_locations = {}; +}; + +} diff --git a/libsolidity/lsp/SemanticTokensBuilder.cpp b/libsolidity/lsp/SemanticTokensBuilder.cpp index b1263139e..dae48c1b5 100644 --- a/libsolidity/lsp/SemanticTokensBuilder.cpp +++ b/libsolidity/lsp/SemanticTokensBuilder.cpp @@ -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) diff --git a/test/libsolidity/lsp/rename/contract.sol b/test/libsolidity/lsp/rename/contract.sol new file mode 100644 index 000000000..5bdc14fe6 --- /dev/null +++ b/test/libsolidity/lsp/rename/contract.sol @@ -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 +// } +// ] +// } +// } diff --git a/test/libsolidity/lsp/rename/function.sol b/test/libsolidity/lsp/rename/function.sol new file mode 100644 index 000000000..ffd8f6b58 --- /dev/null +++ b/test/libsolidity/lsp/rename/function.sol @@ -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 +// } +// ] +// } +// } diff --git a/test/libsolidity/lsp/rename/functionCall.sol b/test/libsolidity/lsp/rename/functionCall.sol new file mode 100644 index 000000000..f8417fab9 --- /dev/null +++ b/test/libsolidity/lsp/rename/functionCall.sol @@ -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 +// } +// ] +// } +// } diff --git a/test/libsolidity/lsp/rename/import_directive.sol b/test/libsolidity/lsp/rename/import_directive.sol new file mode 100644 index 000000000..a6b596866 --- /dev/null +++ b/test/libsolidity/lsp/rename/import_directive.sol @@ -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 +// } +// ] +// } +// } diff --git a/test/libsolidity/lsp/rename/variable.sol b/test/libsolidity/lsp/rename/variable.sol new file mode 100644 index 000000000..39c23bcd7 --- /dev/null +++ b/test/libsolidity/lsp/rename/variable.sol @@ -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 +// } +// ] +// } +// } diff --git a/test/lsp.py b/test/lsp.py index f20e93953..c3e5dcc9d 100755 --- a/test/lsp.py +++ b/test/lsp.py @@ -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 "@". + + Recognized patterns: + { "changes": { "": { "range": "" } } } + { "uri": "", "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)