mirror of
				https://github.com/ethereum/solidity
				synced 2023-10-03 13:03:40 +00:00 
			
		
		
		
	
						commit
						3193dfbb5d
					
				| @ -198,6 +198,19 @@ defaults: | ||||
|         - store_artifacts: *artifacts_test_results | ||||
|         - gitter_notify_failure_unless_pr | ||||
| 
 | ||||
|   - steps_test_lsp: &steps_test_lsp | ||||
|       steps: | ||||
|         - checkout | ||||
|         - attach_workspace: | ||||
|             at: build | ||||
|         - run: | ||||
|             name: Install dependencies | ||||
|             command: pip install --user deepdiff colorama | ||||
|         - run: | ||||
|             name: Executing solc LSP test suite | ||||
|             command: ./test/lsp.py ./build/solc/solc | ||||
|         - gitter_notify_failure_unless_pr | ||||
| 
 | ||||
|   - steps_soltest_all: &steps_soltest_all | ||||
|       steps: | ||||
|         - checkout | ||||
| @ -519,7 +532,7 @@ jobs: | ||||
|           command: apt -q update && apt install -y python3-pip | ||||
|       - run: | ||||
|           name: Install pylint | ||||
|           command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate | ||||
|           command: python3 -m pip install pylint z3-solver pygments-lexer-solidity parsec tabulate deepdiff colorama | ||||
|           # also z3-solver, parsec and tabulate to make sure pylint knows about this module, pygments-lexer-solidity for docs | ||||
|       - run: | ||||
|           name: Linting Python Scripts | ||||
| @ -887,6 +900,10 @@ jobs: | ||||
|     parallelism: 15 # 7 EVM versions, each with/without optimization + 1 ABIv1/@nooptions run | ||||
|     <<: *steps_soltest_all | ||||
| 
 | ||||
|   t_ubu_lsp: &t_ubu_lsp | ||||
|     <<: *base_ubuntu2004_small | ||||
|     <<: *steps_test_lsp | ||||
| 
 | ||||
|   t_archlinux_soltest: &t_archlinux_soltest | ||||
|     <<: *base_archlinux | ||||
|     environment: | ||||
| @ -1288,6 +1305,7 @@ workflows: | ||||
|       - t_ubu_soltest_enforce_yul: *workflow_ubuntu2004 | ||||
|       - b_ubu_clang: *workflow_trigger_on_tags | ||||
|       - t_ubu_clang_soltest: *workflow_ubuntu2004_clang | ||||
|       - t_ubu_lsp: *workflow_ubuntu2004 | ||||
| 
 | ||||
|       # Ubuntu fake release build and tests | ||||
|       - b_ubu_release: *workflow_trigger_on_tags | ||||
|  | ||||
| @ -5,6 +5,7 @@ Language Features: | ||||
| 
 | ||||
| 
 | ||||
| Compiler Features: | ||||
|  * Commandline Interface: Add ``--lsp`` option to get ``solc`` to act as a Language Server (LSP) communicating over stdio. | ||||
| 
 | ||||
| 
 | ||||
| Bugfixes: | ||||
|  | ||||
| @ -155,6 +155,12 @@ set(sources | ||||
| 	interface/StorageLayout.h | ||||
| 	interface/Version.cpp | ||||
| 	interface/Version.h | ||||
| 	lsp/LanguageServer.cpp | ||||
| 	lsp/LanguageServer.h | ||||
| 	lsp/FileRepository.cpp | ||||
| 	lsp/FileRepository.h | ||||
| 	lsp/Transport.cpp | ||||
| 	lsp/Transport.h | ||||
| 	parsing/DocStringParser.cpp | ||||
| 	parsing/DocStringParser.h | ||||
| 	parsing/Parser.cpp | ||||
|  | ||||
							
								
								
									
										64
									
								
								libsolidity/lsp/FileRepository.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								libsolidity/lsp/FileRepository.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| /*
 | ||||
| 	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/FileRepository.h> | ||||
| 
 | ||||
| using namespace std; | ||||
| using namespace solidity; | ||||
| using namespace solidity::lsp; | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
| 
 | ||||
| string stripFilePrefix(string const& _path) | ||||
| { | ||||
| 	if (_path.find("file://") == 0) | ||||
| 		return _path.substr(7); | ||||
| 	else | ||||
| 		return _path; | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| string FileRepository::sourceUnitNameToClientPath(string const& _sourceUnitName) const | ||||
| { | ||||
| 	if (m_sourceUnitNamesToClientPaths.count(_sourceUnitName)) | ||||
| 		return m_sourceUnitNamesToClientPaths.at(_sourceUnitName); | ||||
| 	else if (_sourceUnitName.find("file://") == 0) | ||||
| 		return _sourceUnitName; | ||||
| 	else | ||||
| 		return "file://" + (m_fileReader.basePath() / _sourceUnitName).generic_string(); | ||||
| } | ||||
| 
 | ||||
| string FileRepository::clientPathToSourceUnitName(string const& _path) const | ||||
| { | ||||
| 	return m_fileReader.cliPathToSourceUnitName(stripFilePrefix(_path)); | ||||
| } | ||||
| 
 | ||||
| map<string, string> const& FileRepository::sourceUnits() const | ||||
| { | ||||
| 	return m_fileReader.sourceUnits(); | ||||
| } | ||||
| 
 | ||||
| void FileRepository::setSourceByClientPath(string const& _uri, string _text) | ||||
| { | ||||
| 	// This is needed for uris outside the base path. It can lead to collisions,
 | ||||
| 	// but we need to mostly rewrite this in a future version anyway.
 | ||||
| 	m_sourceUnitNamesToClientPaths.emplace(clientPathToSourceUnitName(_uri), _uri); | ||||
| 	m_fileReader.addOrUpdateFile(stripFilePrefix(_uri), move(_text)); | ||||
| } | ||||
							
								
								
									
										53
									
								
								libsolidity/lsp/FileRepository.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								libsolidity/lsp/FileRepository.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| /*
 | ||||
| 	This file is part of solidity. | ||||
| 
 | ||||
| 	solidity is free software: you can redistribute it and/or modify | ||||
| 	it under the terms of the GNU General Public License as published by | ||||
| 	the Free Software Foundation, either version 3 of the License, or | ||||
| 	(at your option) any later version. | ||||
| 
 | ||||
| 	solidity is distributed in the hope that it will be useful, | ||||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| 	GNU General Public License for more details. | ||||
| 
 | ||||
| 	You should have received a copy of the GNU General Public License | ||||
| 	along with solidity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| */ | ||||
| // SPDX-License-Identifier: GPL-3.0
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <libsolidity/interface/FileReader.h> | ||||
| 
 | ||||
| #include <string> | ||||
| #include <map> | ||||
| 
 | ||||
| namespace solidity::lsp | ||||
| { | ||||
| 
 | ||||
| class FileRepository | ||||
| { | ||||
| public: | ||||
| 	explicit FileRepository(boost::filesystem::path const& _basePath): | ||||
| 		m_fileReader(_basePath) {} | ||||
| 
 | ||||
| 	boost::filesystem::path const& basePath() const { return m_fileReader.basePath(); } | ||||
| 
 | ||||
| 	/// Translates a compiler-internal source unit name to an LSP client path.
 | ||||
| 	std::string sourceUnitNameToClientPath(std::string const& _sourceUnitName) const; | ||||
| 	/// Translates an LSP client path into a compiler-internal source unit name.
 | ||||
| 	std::string clientPathToSourceUnitName(std::string const& _uri) const; | ||||
| 
 | ||||
| 	/// @returns all sources by their compiler-internal source unit name.
 | ||||
| 	std::map<std::string, std::string> const& sourceUnits() const; | ||||
| 	/// Changes the source identified by the LSP client path _uri to _text.
 | ||||
| 	void setSourceByClientPath(std::string const& _uri, std::string _text); | ||||
| 
 | ||||
| 	frontend::ReadCallback::Callback reader() { return m_fileReader.reader(); } | ||||
| 
 | ||||
| private: | ||||
| 	std::map<std::string, std::string> m_sourceUnitNamesToClientPaths; | ||||
| 	frontend::FileReader m_fileReader; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										402
									
								
								libsolidity/lsp/LanguageServer.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										402
									
								
								libsolidity/lsp/LanguageServer.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,402 @@ | ||||
| /*
 | ||||
| 	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/ast/AST.h> | ||||
| #include <libsolidity/ast/ASTUtils.h> | ||||
| #include <libsolidity/ast/ASTVisitor.h> | ||||
| #include <libsolidity/interface/ReadFile.h> | ||||
| #include <libsolidity/interface/StandardCompiler.h> | ||||
| #include <libsolidity/lsp/LanguageServer.h> | ||||
| 
 | ||||
| #include <liblangutil/SourceReferenceExtractor.h> | ||||
| #include <liblangutil/CharStream.h> | ||||
| 
 | ||||
| #include <libsolutil/Visitor.h> | ||||
| #include <libsolutil/JSON.h> | ||||
| 
 | ||||
| #include <boost/exception/diagnostic_information.hpp> | ||||
| #include <boost/filesystem.hpp> | ||||
| #include <boost/algorithm/string/predicate.hpp> | ||||
| 
 | ||||
| #include <fmt/format.h> | ||||
| 
 | ||||
| #include <ostream> | ||||
| #include <string> | ||||
| 
 | ||||
| using namespace std; | ||||
| using namespace std::placeholders; | ||||
| 
 | ||||
| using namespace solidity::lsp; | ||||
| using namespace solidity::langutil; | ||||
| using namespace solidity::frontend; | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
| 
 | ||||
| Json::Value toJson(LineColumn _pos) | ||||
| { | ||||
| 	Json::Value json = Json::objectValue; | ||||
| 	json["line"] = max(_pos.line, 0); | ||||
| 	json["character"] = max(_pos.column, 0); | ||||
| 
 | ||||
| 	return json; | ||||
| } | ||||
| 
 | ||||
| Json::Value toJsonRange(LineColumn const& _start, LineColumn const& _end) | ||||
| { | ||||
| 	Json::Value json; | ||||
| 	json["start"] = toJson(_start); | ||||
| 	json["end"] = toJson(_end); | ||||
| 	return json; | ||||
| } | ||||
| 
 | ||||
| optional<LineColumn> parseLineColumn(Json::Value const& _lineColumn) | ||||
| { | ||||
| 	if (_lineColumn.isObject() && _lineColumn["line"].isInt() && _lineColumn["character"].isInt()) | ||||
| 		return LineColumn{_lineColumn["line"].asInt(), _lineColumn["character"].asInt()}; | ||||
| 	else | ||||
| 		return nullopt; | ||||
| } | ||||
| 
 | ||||
| constexpr int toDiagnosticSeverity(Error::Type _errorType) | ||||
| { | ||||
| 	// 1=Error, 2=Warning, 3=Info, 4=Hint
 | ||||
| 	switch (Error::errorSeverity(_errorType)) | ||||
| 	{ | ||||
| 	case Error::Severity::Error: return 1; | ||||
| 	case Error::Severity::Warning: return 2; | ||||
| 	case Error::Severity::Info: return 3; | ||||
| 	} | ||||
| 	solAssert(false); | ||||
| 	return -1; | ||||
| } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| LanguageServer::LanguageServer(Transport& _transport): | ||||
| 	m_client{_transport}, | ||||
| 	m_handlers{ | ||||
| 		{"$/cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, | ||||
| 		{"cancelRequest", [](auto, auto) {/*nothing for now as we are synchronous */}}, | ||||
| 		{"exit", [this](auto, auto) { m_state = (m_state == State::ShutdownRequested ? State::ExitRequested : State::ExitWithoutShutdown); }}, | ||||
| 		{"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)}, | ||||
| 		{"initialized", [](auto, auto) {}}, | ||||
| 		{"shutdown", [this](auto, auto) { m_state = State::ShutdownRequested; }}, | ||||
| 		{"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _1, _2)}, | ||||
| 		{"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _1, _2)}, | ||||
| 		{"textDocument/didClose", bind(&LanguageServer::handleTextDocumentDidClose, this, _1, _2)}, | ||||
| 		{"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _1, _2)}, | ||||
| 	}, | ||||
| 	m_fileRepository("/" /* basePath */), | ||||
| 	m_compilerStack{m_fileRepository.reader()} | ||||
| { | ||||
| } | ||||
| 
 | ||||
| optional<SourceLocation> LanguageServer::parsePosition( | ||||
| 	string const& _sourceUnitName, | ||||
| 	Json::Value const& _position | ||||
| ) const | ||||
| { | ||||
| 	if (!m_fileRepository.sourceUnits().count(_sourceUnitName)) | ||||
| 		return nullopt; | ||||
| 
 | ||||
| 	if (optional<LineColumn> lineColumn = parseLineColumn(_position)) | ||||
| 		if (optional<int> const offset = CharStream::translateLineColumnToPosition( | ||||
| 			m_fileRepository.sourceUnits().at(_sourceUnitName), | ||||
| 			*lineColumn | ||||
| 		)) | ||||
| 			return SourceLocation{*offset, *offset, make_shared<string>(_sourceUnitName)}; | ||||
| 	return nullopt; | ||||
| } | ||||
| 
 | ||||
| optional<SourceLocation> LanguageServer::parseRange(string const& _sourceUnitName, Json::Value const& _range) const | ||||
| { | ||||
| 	if (!_range.isObject()) | ||||
| 		return nullopt; | ||||
| 	optional<SourceLocation> start = parsePosition(_sourceUnitName, _range["start"]); | ||||
| 	optional<SourceLocation> end = parsePosition(_sourceUnitName, _range["end"]); | ||||
| 	if (!start || !end) | ||||
| 		return nullopt; | ||||
| 	solAssert(*start->sourceName == *end->sourceName); | ||||
| 	start->end = end->end; | ||||
| 	return start; | ||||
| } | ||||
| 
 | ||||
| Json::Value LanguageServer::toRange(SourceLocation const& _location) const | ||||
| { | ||||
| 	if (!_location.hasText()) | ||||
| 		return toJsonRange({}, {}); | ||||
| 
 | ||||
| 	solAssert(_location.sourceName, ""); | ||||
| 	CharStream const& stream = m_compilerStack.charStream(*_location.sourceName); | ||||
| 	LineColumn start = stream.translatePositionToLineColumn(_location.start); | ||||
| 	LineColumn end = stream.translatePositionToLineColumn(_location.end); | ||||
| 	return toJsonRange(start, end); | ||||
| } | ||||
| 
 | ||||
| Json::Value LanguageServer::toJson(SourceLocation const& _location) const | ||||
| { | ||||
| 	solAssert(_location.sourceName); | ||||
| 	Json::Value item = Json::objectValue; | ||||
| 	item["uri"] = m_fileRepository.sourceUnitNameToClientPath(*_location.sourceName); | ||||
| 	item["range"] = toRange(_location); | ||||
| 	return item; | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::changeConfiguration(Json::Value const& _settings) | ||||
| { | ||||
| 	m_settingsObject = _settings; | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::compile() | ||||
| { | ||||
| 	// For files that are not open, we have to take changes on disk into account,
 | ||||
| 	// so we just remove all non-open files.
 | ||||
| 
 | ||||
| 	FileRepository oldRepository(m_fileRepository.basePath()); | ||||
| 	swap(oldRepository, m_fileRepository); | ||||
| 
 | ||||
| 	for (string const& fileName: m_openFiles) | ||||
| 		m_fileRepository.setSourceByClientPath( | ||||
| 			fileName, | ||||
| 			oldRepository.sourceUnits().at(oldRepository.clientPathToSourceUnitName(fileName)) | ||||
| 		); | ||||
| 
 | ||||
| 	// TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty).
 | ||||
| 
 | ||||
| 	m_compilerStack.reset(false); | ||||
| 	m_compilerStack.setSources(m_fileRepository.sourceUnits()); | ||||
| 	m_compilerStack.compile(CompilerStack::State::AnalysisPerformed); | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::compileAndUpdateDiagnostics() | ||||
| { | ||||
| 	compile(); | ||||
| 
 | ||||
| 	// These are the source units we will sent diagnostics to the client for sure,
 | ||||
| 	// even if it is just to clear previous diagnostics.
 | ||||
| 	map<string, Json::Value> diagnosticsBySourceUnit; | ||||
| 	for (string const& sourceUnitName: m_fileRepository.sourceUnits() | ranges::views::keys) | ||||
| 		diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue; | ||||
| 	for (string const& sourceUnitName: m_nonemptyDiagnostics) | ||||
| 		diagnosticsBySourceUnit[sourceUnitName] = Json::arrayValue; | ||||
| 
 | ||||
| 	for (shared_ptr<Error const> const& error: m_compilerStack.errors()) | ||||
| 	{ | ||||
| 		SourceLocation const* location = error->sourceLocation(); | ||||
| 		if (!location || !location->sourceName) | ||||
| 			// LSP only has diagnostics applied to individual files.
 | ||||
| 			continue; | ||||
| 
 | ||||
| 		Json::Value jsonDiag; | ||||
| 		jsonDiag["source"] = "solc"; | ||||
| 		jsonDiag["severity"] = toDiagnosticSeverity(error->type()); | ||||
| 		jsonDiag["code"] = Json::UInt64{error->errorId().error}; | ||||
| 		string message = error->typeName() + ":"; | ||||
| 		if (string const* comment = error->comment()) | ||||
| 			message += " " + *comment; | ||||
| 		jsonDiag["message"] = move(message); | ||||
| 		jsonDiag["range"] = toRange(*location); | ||||
| 
 | ||||
| 		if (auto const* secondary = error->secondarySourceLocation()) | ||||
| 			for (auto&& [secondaryMessage, secondaryLocation]: secondary->infos) | ||||
| 			{ | ||||
| 				Json::Value jsonRelated; | ||||
| 				jsonRelated["message"] = secondaryMessage; | ||||
| 				jsonRelated["location"] = toJson(secondaryLocation); | ||||
| 				jsonDiag["relatedInformation"].append(jsonRelated); | ||||
| 			} | ||||
| 
 | ||||
| 		diagnosticsBySourceUnit[*location->sourceName].append(jsonDiag); | ||||
| 	} | ||||
| 
 | ||||
| 	m_nonemptyDiagnostics.clear(); | ||||
| 	for (auto&& [sourceUnitName, diagnostics]: diagnosticsBySourceUnit) | ||||
| 	{ | ||||
| 		Json::Value params; | ||||
| 		params["uri"] = m_fileRepository.sourceUnitNameToClientPath(sourceUnitName); | ||||
| 		if (!diagnostics.empty()) | ||||
| 			m_nonemptyDiagnostics.insert(sourceUnitName); | ||||
| 		params["diagnostics"] = move(diagnostics); | ||||
| 		m_client.notify("textDocument/publishDiagnostics", move(params)); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| bool LanguageServer::run() | ||||
| { | ||||
| 	while (m_state != State::ExitRequested && m_state != State::ExitWithoutShutdown && !m_client.closed()) | ||||
| 	{ | ||||
| 		MessageID id; | ||||
| 		try | ||||
| 		{ | ||||
| 			optional<Json::Value> const jsonMessage = m_client.receive(); | ||||
| 			if (!jsonMessage) | ||||
| 				continue; | ||||
| 
 | ||||
| 			if ((*jsonMessage)["method"].isString()) | ||||
| 			{ | ||||
| 				string const methodName = (*jsonMessage)["method"].asString(); | ||||
| 				id = (*jsonMessage)["id"]; | ||||
| 
 | ||||
| 				if (auto handler = valueOrDefault(m_handlers, methodName)) | ||||
| 					handler(id, (*jsonMessage)["params"]); | ||||
| 				else | ||||
| 					m_client.error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName); | ||||
| 			} | ||||
| 			else | ||||
| 				m_client.error({}, ErrorCode::ParseError, "\"method\" has to be a string."); | ||||
| 		} | ||||
| 		catch (...) | ||||
| 		{ | ||||
| 			m_client.error(id, ErrorCode::InternalError, "Unhandled exception: "s + boost::current_exception_diagnostic_information()); | ||||
| 		} | ||||
| 	} | ||||
| 	return m_state == State::ExitRequested; | ||||
| } | ||||
| 
 | ||||
| bool LanguageServer::checkServerInitialized(MessageID _id) | ||||
| { | ||||
| 	if (m_state != State::Initialized) | ||||
| 	{ | ||||
| 		m_client.error(_id, ErrorCode::ServerNotInitialized, "Server is not properly initialized."); | ||||
| 		return false; | ||||
| 	} | ||||
| 	else | ||||
| 		return true; | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) | ||||
| { | ||||
| 	if (m_state != State::Started) | ||||
| 	{ | ||||
| 		m_client.error(_id, ErrorCode::RequestFailed, "Initialize called at the wrong time."); | ||||
| 		return; | ||||
| 	} | ||||
| 	m_state = State::Initialized; | ||||
| 
 | ||||
| 	// The default of FileReader is to use `.`, but the path from where the LSP was started
 | ||||
| 	// should not matter.
 | ||||
| 	string rootPath("/"); | ||||
| 	if (Json::Value uri = _args["rootUri"]) | ||||
| 	{ | ||||
| 		rootPath = uri.asString(); | ||||
| 		if (!boost::starts_with(rootPath, "file://")) | ||||
| 		{ | ||||
| 			m_client.error(_id, ErrorCode::InvalidParams, "rootUri only supports file URI scheme."); | ||||
| 			return; | ||||
| 		} | ||||
| 		rootPath = rootPath.substr(7); | ||||
| 	} | ||||
| 	else if (Json::Value rootPath = _args["rootPath"]) | ||||
| 		rootPath = rootPath.asString(); | ||||
| 
 | ||||
| 	m_fileRepository = FileRepository(boost::filesystem::path(rootPath)); | ||||
| 	if (_args["initializationOptions"].isObject()) | ||||
| 		changeConfiguration(_args["initializationOptions"]); | ||||
| 
 | ||||
| 	Json::Value replyArgs; | ||||
| 	replyArgs["serverInfo"]["name"] = "solc"; | ||||
| 	replyArgs["serverInfo"]["version"] = string(VersionNumber); | ||||
| 	replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; | ||||
| 	replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental
 | ||||
| 
 | ||||
| 	m_client.reply(_id, move(replyArgs)); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| void LanguageServer::handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args) | ||||
| { | ||||
| 	if (!checkServerInitialized(_id)) | ||||
| 		return; | ||||
| 
 | ||||
| 	if (_args["settings"].isObject()) | ||||
| 		changeConfiguration(_args["settings"]); | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args) | ||||
| { | ||||
| 	if (!checkServerInitialized(_id)) | ||||
| 		return; | ||||
| 
 | ||||
| 	if (!_args["textDocument"]) | ||||
| 		m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing."); | ||||
| 
 | ||||
| 	string text = _args["textDocument"]["text"].asString(); | ||||
| 	string uri = _args["textDocument"]["uri"].asString(); | ||||
| 	m_openFiles.insert(uri); | ||||
| 	m_fileRepository.setSourceByClientPath(uri, move(text)); | ||||
| 	compileAndUpdateDiagnostics(); | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::handleTextDocumentDidChange(MessageID _id, Json::Value const& _args) | ||||
| { | ||||
| 	if (!checkServerInitialized(_id)) | ||||
| 		return; | ||||
| 
 | ||||
| 	string const uri = _args["textDocument"]["uri"].asString(); | ||||
| 
 | ||||
| 	for (Json::Value jsonContentChange: _args["contentChanges"]) | ||||
| 	{ | ||||
| 		if (!jsonContentChange.isObject()) | ||||
| 		{ | ||||
| 			m_client.error(_id, ErrorCode::RequestFailed, "Invalid content reference."); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		string const sourceUnitName = m_fileRepository.clientPathToSourceUnitName(uri); | ||||
| 		if (!m_fileRepository.sourceUnits().count(sourceUnitName)) | ||||
| 		{ | ||||
| 			m_client.error(_id, ErrorCode::RequestFailed, "Unknown file: " + uri); | ||||
| 			return; | ||||
| 		} | ||||
| 
 | ||||
| 		string text = jsonContentChange["text"].asString(); | ||||
| 		if (jsonContentChange["range"].isObject()) // otherwise full content update
 | ||||
| 		{ | ||||
| 			optional<SourceLocation> change = parseRange(sourceUnitName, jsonContentChange["range"]); | ||||
| 			if (!change || !change->hasText()) | ||||
| 			{ | ||||
| 				m_client.error( | ||||
| 					_id, | ||||
| 					ErrorCode::RequestFailed, | ||||
| 					"Invalid source range: " + jsonCompactPrint(jsonContentChange["range"]) | ||||
| 				); | ||||
| 				return; | ||||
| 			} | ||||
| 			string buffer = m_fileRepository.sourceUnits().at(sourceUnitName); | ||||
| 			buffer.replace(static_cast<size_t>(change->start), static_cast<size_t>(change->end - change->start), move(text)); | ||||
| 			text = move(buffer); | ||||
| 		} | ||||
| 		m_fileRepository.setSourceByClientPath(uri, move(text)); | ||||
| 	} | ||||
| 
 | ||||
| 	compileAndUpdateDiagnostics(); | ||||
| } | ||||
| 
 | ||||
| void LanguageServer::handleTextDocumentDidClose(MessageID _id, Json::Value const& _args) | ||||
| { | ||||
| 	if (!checkServerInitialized(_id)) | ||||
| 		return; | ||||
| 
 | ||||
| 	if (!_args["textDocument"]) | ||||
| 		m_client.error(_id, ErrorCode::RequestFailed, "Text document parameter missing."); | ||||
| 
 | ||||
| 	string uri = _args["textDocument"]["uri"].asString(); | ||||
| 	m_openFiles.erase(uri); | ||||
| 
 | ||||
| 	compileAndUpdateDiagnostics(); | ||||
| } | ||||
							
								
								
									
										110
									
								
								libsolidity/lsp/LanguageServer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								libsolidity/lsp/LanguageServer.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,110 @@ | ||||
| /*
 | ||||
| 	This file is part of solidity. | ||||
| 
 | ||||
| 	solidity is free software: you can redistribute it and/or modify | ||||
| 	it under the terms of the GNU General Public License as published by | ||||
| 	the Free Software Foundation, either version 3 of the License, or | ||||
| 	(at your option) any later version. | ||||
| 
 | ||||
| 	solidity is distributed in the hope that it will be useful, | ||||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| 	GNU General Public License for more details. | ||||
| 
 | ||||
| 	You should have received a copy of the GNU General Public License | ||||
| 	along with solidity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| */ | ||||
| // SPDX-License-Identifier: GPL-3.0
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <libsolidity/lsp/Transport.h> | ||||
| #include <libsolidity/lsp/FileRepository.h> | ||||
| #include <libsolidity/interface/CompilerStack.h> | ||||
| #include <libsolidity/interface/FileReader.h> | ||||
| 
 | ||||
| #include <json/value.h> | ||||
| 
 | ||||
| #include <functional> | ||||
| #include <map> | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <vector> | ||||
| 
 | ||||
| namespace solidity::lsp | ||||
| { | ||||
| 
 | ||||
| enum class ErrorCode; | ||||
| 
 | ||||
| /**
 | ||||
|  * Solidity Language Server, managing one LSP client. | ||||
|  * This implements a subset of LSP version 3.16 that can be found at: | ||||
|  * https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/
 | ||||
|  */ | ||||
| class LanguageServer | ||||
| { | ||||
| public: | ||||
| 	/// @param _transport Customizable transport layer.
 | ||||
| 	explicit LanguageServer(Transport& _transport); | ||||
| 
 | ||||
| 	/// Re-compiles the project and updates the diagnostics pushed to the client.
 | ||||
| 	void compileAndUpdateDiagnostics(); | ||||
| 
 | ||||
| 	/// Loops over incoming messages via the transport layer until shutdown condition is met.
 | ||||
| 	///
 | ||||
| 	/// The standard shutdown condition is when the maximum number of consecutive failures
 | ||||
| 	/// has been exceeded.
 | ||||
| 	///
 | ||||
| 	/// @return boolean indicating normal or abnormal termination.
 | ||||
| 	bool run(); | ||||
| 
 | ||||
| private: | ||||
| 	/// Checks if the server is initialized (to be used by messages that need it to be initialized).
 | ||||
| 	/// Reports an error and returns false if not.
 | ||||
| 	bool checkServerInitialized(MessageID _id); | ||||
| 	void handleInitialize(MessageID _id, Json::Value const& _args); | ||||
| 	void handleWorkspaceDidChangeConfiguration(MessageID _id, Json::Value const& _args); | ||||
| 	void handleTextDocumentDidOpen(MessageID _id, Json::Value const& _args); | ||||
| 	void handleTextDocumentDidChange(MessageID _id, Json::Value const& _args); | ||||
| 	void handleTextDocumentDidClose(MessageID _id, Json::Value const& _args); | ||||
| 
 | ||||
| 	/// Invoked when the server user-supplied configuration changes (initiated by the client).
 | ||||
| 	void changeConfiguration(Json::Value const&); | ||||
| 
 | ||||
| 	/// Compile everything until after analysis phase.
 | ||||
| 	void compile(); | ||||
| 
 | ||||
| 	std::optional<langutil::SourceLocation> parsePosition( | ||||
| 		std::string const& _sourceUnitName, | ||||
| 		Json::Value const& _position | ||||
| 	) const; | ||||
| 	/// @returns the source location given a source unit name and an LSP Range object,
 | ||||
| 	/// or nullopt on failure.
 | ||||
| 	std::optional<langutil::SourceLocation> parseRange( | ||||
| 		std::string const& _sourceUnitName, | ||||
| 		Json::Value const& _range | ||||
| 	) const; | ||||
| 	Json::Value toRange(langutil::SourceLocation const& _location) const; | ||||
| 	Json::Value toJson(langutil::SourceLocation const& _location) const; | ||||
| 
 | ||||
| 	// LSP related member fields
 | ||||
| 	using MessageHandler = std::function<void(MessageID, Json::Value const&)>; | ||||
| 
 | ||||
| 	enum class State { Started, Initialized, ShutdownRequested, ExitRequested, ExitWithoutShutdown }; | ||||
| 	State m_state = State::Started; | ||||
| 
 | ||||
| 	Transport& m_client; | ||||
| 	std::map<std::string, MessageHandler> m_handlers; | ||||
| 
 | ||||
| 	/// Set of files known to be open by the client.
 | ||||
| 	std::set<std::string> m_openFiles; | ||||
| 	/// Set of source unit names for which we sent diagnostics to the client in the last iteration.
 | ||||
| 	std::set<std::string> m_nonemptyDiagnostics; | ||||
| 	FileRepository m_fileRepository; | ||||
| 
 | ||||
| 	frontend::CompilerStack m_compilerStack; | ||||
| 
 | ||||
| 	/// User-supplied custom configuration settings (such as EVM version).
 | ||||
| 	Json::Value m_settingsObject; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										141
									
								
								libsolidity/lsp/Transport.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								libsolidity/lsp/Transport.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,141 @@ | ||||
| /*
 | ||||
| 	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/Transport.h> | ||||
| 
 | ||||
| #include <libsolutil/JSON.h> | ||||
| #include <libsolutil/Visitor.h> | ||||
| #include <libsolutil/CommonIO.h> | ||||
| #include <liblangutil/Exceptions.h> | ||||
| 
 | ||||
| #include <boost/algorithm/string.hpp> | ||||
| 
 | ||||
| #include <iostream> | ||||
| #include <sstream> | ||||
| 
 | ||||
| using namespace std; | ||||
| using namespace solidity::lsp; | ||||
| 
 | ||||
| IOStreamTransport::IOStreamTransport(istream& _in, ostream& _out): | ||||
| 	m_input{_in}, | ||||
| 	m_output{_out} | ||||
| { | ||||
| } | ||||
| 
 | ||||
| IOStreamTransport::IOStreamTransport(): | ||||
| 	IOStreamTransport(cin, cout) | ||||
| { | ||||
| } | ||||
| 
 | ||||
| bool IOStreamTransport::closed() const noexcept | ||||
| { | ||||
| 	return m_input.eof(); | ||||
| } | ||||
| 
 | ||||
| optional<Json::Value> IOStreamTransport::receive() | ||||
| { | ||||
| 	auto const headers = parseHeaders(); | ||||
| 	if (!headers) | ||||
| 	{ | ||||
| 		error({}, ErrorCode::ParseError, "Could not parse RPC headers."); | ||||
| 		return nullopt; | ||||
| 	} | ||||
| 
 | ||||
| 	if (!headers->count("content-length")) | ||||
| 	{ | ||||
| 		error({}, ErrorCode::ParseError, "No content-length header found."); | ||||
| 		return nullopt; | ||||
| 	} | ||||
| 
 | ||||
| 	string const data = util::readBytes(m_input, stoul(headers->at("content-length"))); | ||||
| 
 | ||||
| 	Json::Value jsonMessage; | ||||
| 	string jsonParsingErrors; | ||||
| 	solidity::util::jsonParseStrict(data, jsonMessage, &jsonParsingErrors); | ||||
| 	if (!jsonParsingErrors.empty() || !jsonMessage || !jsonMessage.isObject()) | ||||
| 	{ | ||||
| 		error({}, ErrorCode::ParseError, "Could not parse RPC JSON payload. " + jsonParsingErrors); | ||||
| 		return nullopt; | ||||
| 	} | ||||
| 
 | ||||
| 	return {move(jsonMessage)}; | ||||
| } | ||||
| 
 | ||||
| void IOStreamTransport::notify(string _method, Json::Value _message) | ||||
| { | ||||
| 	Json::Value json; | ||||
| 	json["method"] = move(_method); | ||||
| 	json["params"] = move(_message); | ||||
| 	send(move(json)); | ||||
| } | ||||
| 
 | ||||
| void IOStreamTransport::reply(MessageID _id, Json::Value _message) | ||||
| { | ||||
| 	Json::Value json; | ||||
| 	json["result"] = move(_message); | ||||
| 	send(move(json), _id); | ||||
| } | ||||
| 
 | ||||
| void IOStreamTransport::error(MessageID _id, ErrorCode _code, string _message) | ||||
| { | ||||
| 	Json::Value json; | ||||
| 	json["error"]["code"] = static_cast<int>(_code); | ||||
| 	json["error"]["message"] = move(_message); | ||||
| 	send(move(json), _id); | ||||
| } | ||||
| 
 | ||||
| void IOStreamTransport::send(Json::Value _json, MessageID _id) | ||||
| { | ||||
| 	solAssert(_json.isObject()); | ||||
| 	_json["jsonrpc"] = "2.0"; | ||||
| 	if (_id != Json::nullValue) | ||||
| 		_json["id"] = _id; | ||||
| 
 | ||||
| 	string const jsonString = solidity::util::jsonCompactPrint(_json); | ||||
| 
 | ||||
| 	m_output << "Content-Length: " << jsonString.size() << "\r\n"; | ||||
| 	m_output << "\r\n"; | ||||
| 	m_output << jsonString; | ||||
| 
 | ||||
| 	m_output.flush(); | ||||
| } | ||||
| 
 | ||||
| optional<map<string, string>> IOStreamTransport::parseHeaders() | ||||
| { | ||||
| 	map<string, string> headers; | ||||
| 
 | ||||
| 	while (true) | ||||
| 	{ | ||||
| 		string line; | ||||
| 		getline(m_input, line); | ||||
| 		if (boost::trim_copy(line).empty()) | ||||
| 			break; | ||||
| 
 | ||||
| 		auto const delimiterPos = line.find(':'); | ||||
| 		if (delimiterPos == string::npos) | ||||
| 			return nullopt; | ||||
| 
 | ||||
| 		string name = boost::to_lower_copy(line.substr(0, delimiterPos)); | ||||
| 		string value = line.substr(delimiterPos + 1); | ||||
| 		if (!headers.emplace( | ||||
| 			boost::trim_copy(name), | ||||
| 			boost::trim_copy(value) | ||||
| 		).second) | ||||
| 			return nullopt; | ||||
| 	} | ||||
| 	return {move(headers)}; | ||||
| } | ||||
							
								
								
									
										101
									
								
								libsolidity/lsp/Transport.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								libsolidity/lsp/Transport.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,101 @@ | ||||
| /*
 | ||||
| 	This file is part of solidity. | ||||
| 
 | ||||
| 	solidity is free software: you can redistribute it and/or modify | ||||
| 	it under the terms of the GNU General Public License as published by | ||||
| 	the Free Software Foundation, either version 3 of the License, or | ||||
| 	(at your option) any later version. | ||||
| 
 | ||||
| 	solidity is distributed in the hope that it will be useful, | ||||
| 	but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| 	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| 	GNU General Public License for more details. | ||||
| 
 | ||||
| 	You should have received a copy of the GNU General Public License | ||||
| 	along with solidity.  If not, see <http://www.gnu.org/licenses/>.
 | ||||
| */ | ||||
| // SPDX-License-Identifier: GPL-3.0
 | ||||
| #pragma once | ||||
| 
 | ||||
| #include <json/value.h> | ||||
| 
 | ||||
| #include <functional> | ||||
| #include <iosfwd> | ||||
| #include <map> | ||||
| #include <optional> | ||||
| #include <string> | ||||
| #include <string_view> | ||||
| #include <variant> | ||||
| 
 | ||||
| namespace solidity::lsp | ||||
| { | ||||
| 
 | ||||
| using MessageID = Json::Value; | ||||
| 
 | ||||
| enum class ErrorCode | ||||
| { | ||||
| 	// Defined by JSON RPC
 | ||||
| 	ParseError = -32700, | ||||
| 	MethodNotFound = -32601, | ||||
| 	InvalidParams = -32602, | ||||
| 	InternalError = -32603, | ||||
| 
 | ||||
| 	// Defined by the protocol.
 | ||||
| 	ServerNotInitialized = -32002, | ||||
| 	RequestFailed = -32803 | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * Transport layer API | ||||
|  * | ||||
|  * The transport layer API is abstracted to make LSP more testable as well as | ||||
|  * this way it could be possible to support other transports (HTTP for example) easily. | ||||
|  */ | ||||
| class Transport | ||||
| { | ||||
| public: | ||||
| 	virtual ~Transport() = default; | ||||
| 
 | ||||
| 	virtual bool closed() const noexcept = 0; | ||||
| 	virtual std::optional<Json::Value> receive() = 0; | ||||
| 	virtual void notify(std::string _method, Json::Value _params) = 0; | ||||
| 	virtual void reply(MessageID _id, Json::Value _result) = 0; | ||||
| 	virtual void error(MessageID _id, ErrorCode _code, std::string _message) = 0; | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  * LSP Transport using JSON-RPC over iostreams. | ||||
|  */ | ||||
| class IOStreamTransport: public Transport | ||||
| { | ||||
| public: | ||||
| 	/// Constructs a standard stream transport layer.
 | ||||
| 	///
 | ||||
| 	/// @param _in for example std::cin (stdin)
 | ||||
| 	/// @param _out for example std::cout (stdout)
 | ||||
| 	IOStreamTransport(std::istream& _in, std::ostream& _out); | ||||
| 
 | ||||
| 	// Constructs a JSON transport using standard I/O streams.
 | ||||
| 	IOStreamTransport(); | ||||
| 
 | ||||
| 	bool closed() const noexcept override; | ||||
| 	std::optional<Json::Value> receive() override; | ||||
| 	void notify(std::string _method, Json::Value _params) override; | ||||
| 	void reply(MessageID _id, Json::Value _result) override; | ||||
| 	void error(MessageID _id, ErrorCode _code, std::string _message) override; | ||||
| 
 | ||||
| protected: | ||||
| 	/// Sends an arbitrary raw message to the client.
 | ||||
| 	///
 | ||||
| 	/// Used by the notify/reply/error function family.
 | ||||
| 	virtual void send(Json::Value _message, MessageID _id = Json::nullValue); | ||||
| 
 | ||||
| 	/// Parses header section from the client including message-delimiting empty line.
 | ||||
| 	std::optional<std::map<std::string, std::string>> parseHeaders(); | ||||
| 
 | ||||
| private: | ||||
| 	std::istream& m_input; | ||||
| 	std::ostream& m_output; | ||||
| }; | ||||
| 
 | ||||
| } | ||||
| @ -83,6 +83,9 @@ done | ||||
| printTask "Testing Python scripts..." | ||||
| "$REPO_ROOT/test/pyscriptTests.py" | ||||
| 
 | ||||
| printTask "Testing LSP..." | ||||
| "$REPO_ROOT/scripts/test_solidity_lsp.py" "${SOLIDITY_BUILD_DIR}/solc/solc" | ||||
| 
 | ||||
| printTask "Running commandline tests..." | ||||
| # Only run in parallel if this is run on CI infrastructure | ||||
| if [[ -n "$CI" ]] | ||||
|  | ||||
| @ -38,6 +38,8 @@ | ||||
| #include <libsolidity/interface/DebugSettings.h> | ||||
| #include <libsolidity/interface/ImportRemapper.h> | ||||
| #include <libsolidity/interface/StorageLayout.h> | ||||
| #include <libsolidity/lsp/LanguageServer.h> | ||||
| #include <libsolidity/lsp/Transport.h> | ||||
| 
 | ||||
| #include <libyul/AssemblyStack.h> | ||||
| 
 | ||||
| @ -56,6 +58,7 @@ | ||||
| #include <libsolutil/JSON.h> | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <fstream> | ||||
| #include <memory> | ||||
| 
 | ||||
| #include <range/v3/view/map.hpp> | ||||
| @ -499,7 +502,11 @@ void CommandLineInterface::readInputFiles() | ||||
| 			m_fileReader.setStdin(readUntilEnd(m_sin)); | ||||
| 	} | ||||
| 
 | ||||
| 	if (m_fileReader.sourceUnits().empty() && !m_standardJsonInput.has_value()) | ||||
| 	if ( | ||||
| 		m_options.input.mode != InputMode::LanguageServer && | ||||
| 		m_fileReader.sourceUnits().empty() && | ||||
| 		!m_standardJsonInput.has_value() | ||||
| 	) | ||||
| 		solThrow(CommandLineValidationError, "All specified input files either do not exist or are not regular files."); | ||||
| } | ||||
| 
 | ||||
| @ -624,6 +631,9 @@ void CommandLineInterface::processInput() | ||||
| 		m_standardJsonInput.reset(); | ||||
| 		break; | ||||
| 	} | ||||
| 	case InputMode::LanguageServer: | ||||
| 		serveLSP(); | ||||
| 		break; | ||||
| 	case InputMode::Assembler: | ||||
| 		assemble(m_options.assembly.inputLanguage, m_options.assembly.targetMachine); | ||||
| 		break; | ||||
| @ -884,6 +894,13 @@ void CommandLineInterface::handleAst() | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| void CommandLineInterface::serveLSP() | ||||
| { | ||||
| 	lsp::IOStreamTransport transport; | ||||
| 	if (!lsp::LanguageServer{transport}.run()) | ||||
| 		solThrow(CommandLineExecutionError, "LSP terminated abnormally."); | ||||
| } | ||||
| 
 | ||||
| void CommandLineInterface::link() | ||||
| { | ||||
| 	solAssert(m_options.input.mode == InputMode::Linker, ""); | ||||
|  | ||||
| @ -82,6 +82,7 @@ private: | ||||
| 	void printVersion(); | ||||
| 	void printLicense(); | ||||
| 	void compile(); | ||||
| 	void serveLSP(); | ||||
| 	void link(); | ||||
| 	void writeLinkedFiles(); | ||||
| 	/// @returns the ``// <identifier> -> name`` hint for library placeholders.
 | ||||
|  | ||||
| @ -59,6 +59,7 @@ static string const g_strIPFS = "ipfs"; | ||||
| static string const g_strLicense = "license"; | ||||
| static string const g_strLibraries = "libraries"; | ||||
| static string const g_strLink = "link"; | ||||
| static string const g_strLSP = "lsp"; | ||||
| static string const g_strMachine = "machine"; | ||||
| static string const g_strMetadataHash = "metadata-hash"; | ||||
| static string const g_strMetadataLiteral = "metadata-literal"; | ||||
| @ -135,6 +136,7 @@ static map<InputMode, string> const g_inputModeName = { | ||||
| 	{InputMode::Assembler, "assembler"}, | ||||
| 	{InputMode::StandardJson, "standard JSON"}, | ||||
| 	{InputMode::Linker, "linker"}, | ||||
| 	{InputMode::LanguageServer, "language server (LSP)"}, | ||||
| }; | ||||
| 
 | ||||
| void CommandLineParser::checkMutuallyExclusive(vector<string> const& _optionNames) | ||||
| @ -455,6 +457,7 @@ void CommandLineParser::parseOutputSelection() | ||||
| 		case InputMode::Help: | ||||
| 		case InputMode::License: | ||||
| 		case InputMode::Version: | ||||
| 		case InputMode::LanguageServer: | ||||
| 			solAssert(false); | ||||
| 		case InputMode::Compiler: | ||||
| 		case InputMode::CompilerWithASTImport: | ||||
| @ -633,6 +636,11 @@ General Information)").c_str(), | ||||
| 			"Supported Inputs is the output of the --" + g_strStandardJSON + " or the one produced by " | ||||
| 			"--" + g_strCombinedJson + " " + CombinedJsonRequests::componentName(&CombinedJsonRequests::ast)).c_str() | ||||
| 		) | ||||
| 		( | ||||
| 			g_strLSP.c_str(), | ||||
| 			"Switch to language server mode (\"LSP\"). Allows the compiler to be used as an analysis backend " | ||||
| 			"for your favourite IDE." | ||||
| 		) | ||||
| 	; | ||||
| 	desc.add(alternativeInputModes); | ||||
| 
 | ||||
| @ -865,6 +873,7 @@ void CommandLineParser::processArgs() | ||||
| 		g_strStrictAssembly, | ||||
| 		g_strYul, | ||||
| 		g_strImportAst, | ||||
| 		g_strLSP | ||||
| 	}); | ||||
| 
 | ||||
| 	if (m_args.count(g_strHelp) > 0) | ||||
| @ -875,6 +884,8 @@ void CommandLineParser::processArgs() | ||||
| 		m_options.input.mode = InputMode::Version; | ||||
| 	else if (m_args.count(g_strStandardJSON) > 0) | ||||
| 		m_options.input.mode = InputMode::StandardJson; | ||||
| 	else if (m_args.count(g_strLSP)) | ||||
| 		m_options.input.mode = InputMode::LanguageServer; | ||||
| 	else if (m_args.count(g_strAssemble) > 0 || m_args.count(g_strStrictAssembly) > 0 || m_args.count(g_strYul) > 0) | ||||
| 		m_options.input.mode = InputMode::Assembler; | ||||
| 	else if (m_args.count(g_strLink) > 0) | ||||
| @ -910,6 +921,9 @@ void CommandLineParser::processArgs() | ||||
| 			joinOptionNames(invalidOptionsForCurrentInputMode) | ||||
| 		); | ||||
| 
 | ||||
| 	if (m_options.input.mode == InputMode::LanguageServer) | ||||
| 		return; | ||||
| 
 | ||||
| 	checkMutuallyExclusive({g_strColor, g_strNoColor}); | ||||
| 
 | ||||
| 	array<string, 9> const conflictingWithStopAfter{ | ||||
|  | ||||
| @ -56,6 +56,7 @@ enum class InputMode | ||||
| 	StandardJson, | ||||
| 	Linker, | ||||
| 	Assembler, | ||||
| 	LanguageServer | ||||
| }; | ||||
| 
 | ||||
| struct CompilerOutputs | ||||
|  | ||||
							
								
								
									
										6
									
								
								test/libsolidity/lsp/didChange_template.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								test/libsolidity/lsp/didChange_template.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| contract C | ||||
| { | ||||
| } | ||||
							
								
								
									
										12
									
								
								test/libsolidity/lsp/didOpen_with_import.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								test/libsolidity/lsp/didOpen_with_import.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| import './lib.sol'; | ||||
| 
 | ||||
| contract C | ||||
| { | ||||
|     function f(uint a, uint b) public pure returns (uint) | ||||
|     { | ||||
|         return Lib.add(2 * a, b); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								test/libsolidity/lsp/lib.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								test/libsolidity/lsp/lib.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| library Lib | ||||
| { | ||||
|     function add(uint a, uint b) public pure returns (uint result) | ||||
|     { | ||||
|         result = a + b; | ||||
|     } | ||||
| 
 | ||||
|     function warningWithUnused() public pure | ||||
|     { | ||||
|         uint unused; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										18
									
								
								test/libsolidity/lsp/publish_diagnostics_1.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								test/libsolidity/lsp/publish_diagnostics_1.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| contract MyContract | ||||
| { | ||||
|     constructor() | ||||
|     { | ||||
|         uint unused; // [Warning 2072] Unused local variable. | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| contract D | ||||
| { | ||||
|     function main() public payable returns (uint) | ||||
|     { | ||||
|         MyContract c = new MyContract(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								test/libsolidity/lsp/publish_diagnostics_2.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								test/libsolidity/lsp/publish_diagnostics_2.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| contract C | ||||
| { | ||||
|     function makeSomeError() public pure returns (uint res) | ||||
|     { | ||||
|         uint x = "hi"; | ||||
|         return; | ||||
|         res = 2; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| contract D | ||||
| { | ||||
|     function main() public payable returns (uint) | ||||
|     { | ||||
|         C c = new C(); | ||||
|         return c.makeSomeError(2, 3); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										10
									
								
								test/libsolidity/lsp/publish_diagnostics_3.sol
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/libsolidity/lsp/publish_diagnostics_3.sol
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| // SPDX-License-Identifier: UNLICENSED | ||||
| pragma solidity >=0.8.0; | ||||
| 
 | ||||
| abstract contract A { | ||||
|     function a() public virtual; | ||||
| } | ||||
| 
 | ||||
| contract B is A | ||||
| { | ||||
| } | ||||
							
								
								
									
										874
									
								
								test/lsp.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										874
									
								
								test/lsp.py
									
									
									
									
									
										Executable file
									
								
							| @ -0,0 +1,874 @@ | ||||
| #!/usr/bin/env python3 | ||||
| 
 | ||||
| import argparse | ||||
| import fnmatch | ||||
| import json | ||||
| import os | ||||
| import subprocess | ||||
| import traceback | ||||
| 
 | ||||
| from typing import Any, List, Optional, Tuple, Union | ||||
| 
 | ||||
| import colorama # Enables the use of SGR & CUP terminal VT sequences on Windows. | ||||
| from deepdiff import DeepDiff | ||||
| 
 | ||||
| # {{{ JsonRpcProcess | ||||
| class BadHeader(Exception): | ||||
|     def __init__(self, msg: str): | ||||
|         super().__init__("Bad header: " + msg) | ||||
| 
 | ||||
| class JsonRpcProcess: | ||||
|     exe_path: str | ||||
|     exe_args: List[str] | ||||
|     process: subprocess.Popen | ||||
|     trace_io: bool | ||||
| 
 | ||||
|     def __init__(self, exe_path: str, exe_args: List[str], trace_io: bool = True): | ||||
|         self.exe_path = exe_path | ||||
|         self.exe_args = exe_args | ||||
|         self.trace_io = trace_io | ||||
| 
 | ||||
|     def __enter__(self): | ||||
|         self.process = subprocess.Popen( | ||||
|             [self.exe_path, *self.exe_args], | ||||
|             stdin=subprocess.PIPE, | ||||
|             stdout=subprocess.PIPE, | ||||
|             stderr=subprocess.PIPE | ||||
|         ) | ||||
|         return self | ||||
| 
 | ||||
|     def __exit__(self, exception_type, exception_value, traceback) -> None: | ||||
|         self.process.kill() | ||||
|         self.process.wait(timeout=2.0) | ||||
| 
 | ||||
|     def trace(self, topic: str, message: str) -> None: | ||||
|         if self.trace_io: | ||||
|             print(f"{SGR_TRACE}{topic}:{SGR_RESET} {message}") | ||||
| 
 | ||||
|     def receive_message(self) -> Union[None, dict]: | ||||
|         # Note, we should make use of timeout to avoid infinite blocking if nothing is received. | ||||
|         CONTENT_LENGTH_HEADER = "Content-Length: " | ||||
|         CONTENT_TYPE_HEADER = "Content-Type: " | ||||
|         if self.process.stdout == None: | ||||
|             return None | ||||
|         message_size = None | ||||
|         while True: | ||||
|             # read header | ||||
|             line = self.process.stdout.readline() | ||||
|             if line == '': | ||||
|                 # server quit | ||||
|                 return None | ||||
|             line = line.decode("utf-8") | ||||
|             if not line.endswith("\r\n"): | ||||
|                 raise BadHeader("missing newline") | ||||
|             # remove the "\r\n" | ||||
|             line = line[:-2] | ||||
|             if line == '': | ||||
|                 break # done with the headers | ||||
|             if line.startswith(CONTENT_LENGTH_HEADER): | ||||
|                 line = line[len(CONTENT_LENGTH_HEADER):] | ||||
|                 if not line.isdigit(): | ||||
|                     raise BadHeader("size is not int") | ||||
|                 message_size = int(line) | ||||
|             elif line.startswith(CONTENT_TYPE_HEADER): | ||||
|                 # nothing todo with type for now. | ||||
|                 pass | ||||
|             else: | ||||
|                 raise BadHeader("unknown header") | ||||
|         if message_size is None: | ||||
|             raise BadHeader("missing size") | ||||
|         rpc_message = self.process.stdout.read(message_size).decode("utf-8") | ||||
|         json_object = json.loads(rpc_message) | ||||
|         self.trace('receive_message', json.dumps(json_object, indent=4, sort_keys=True)) | ||||
|         return json_object | ||||
| 
 | ||||
|     def send_message(self, method_name: str, params: Optional[dict]) -> None: | ||||
|         if self.process.stdin == None: | ||||
|             return | ||||
|         message = { | ||||
|             'jsonrpc': '2.0', | ||||
|             'method': method_name, | ||||
|             'params': params | ||||
|         } | ||||
|         json_string = json.dumps(obj=message) | ||||
|         rpc_message = f"Content-Length: {len(json_string)}\r\n\r\n{json_string}" | ||||
|         self.trace(f'send_message ({method_name})', json.dumps(message, indent=4, sort_keys=True)) | ||||
|         self.process.stdin.write(rpc_message.encode("utf-8")) | ||||
|         self.process.stdin.flush() | ||||
| 
 | ||||
|     def call_method(self, method_name: str, params: Optional[dict]) -> Any: | ||||
|         self.send_message(method_name, params) | ||||
|         return self.receive_message() | ||||
| 
 | ||||
|     def send_notification(self, name: str, params: Optional[dict] = None) -> None: | ||||
|         self.send_message(name, params) | ||||
| 
 | ||||
| # }}} | ||||
| 
 | ||||
| SGR_RESET = '\033[m' | ||||
| SGR_TRACE = '\033[1;36m' | ||||
| SGR_NOTICE = '\033[1;35m' | ||||
| SGR_TEST_BEGIN = '\033[1;33m' | ||||
| SGR_ASSERT_BEGIN = '\033[1;34m' | ||||
| SGR_STATUS_OKAY = '\033[1;32m' | ||||
| SGR_STATUS_FAIL = '\033[1;31m' | ||||
| 
 | ||||
| class ExpectationFailed(Exception): | ||||
|     def __init__(self, actual, expected): | ||||
|         self.actual = actual | ||||
|         self.expected = expected | ||||
|         diff = DeepDiff(actual, expected) | ||||
|         super().__init__( | ||||
|             f"Expectation failed.\n\tExpected {expected}\n\tbut got {actual}.\n\t{diff}" | ||||
|         ) | ||||
| 
 | ||||
| def create_cli_parser() -> argparse.ArgumentParser: | ||||
|     parser = argparse.ArgumentParser(description="Solidity LSP Test suite") | ||||
|     parser.set_defaults(trace_io=False) | ||||
|     parser.add_argument( | ||||
|         "-T, --trace-io", | ||||
|         dest="trace_io", | ||||
|         action="store_true", | ||||
|         help="Be more verbose by also printing assertions." | ||||
|     ) | ||||
|     parser.set_defaults(print_assertions=False) | ||||
|     parser.add_argument( | ||||
|         "-v, --print-assertions", | ||||
|         dest="print_assertions", | ||||
|         action="store_true", | ||||
|         help="Be more verbose by also printing assertions." | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "-t, --test-pattern", | ||||
|         dest="test_pattern", | ||||
|         type=str, | ||||
|         default="*", | ||||
|         help="Filters all available tests by matching against this test pattern (using globbing)", | ||||
|         nargs="?" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "solc_path", | ||||
|         type=str, | ||||
|         default="solc", | ||||
|         help="Path to solc binary to test against", | ||||
|         nargs="?" | ||||
|     ) | ||||
|     parser.add_argument( | ||||
|         "project_root_dir", | ||||
|         type=str, | ||||
|         default=f"{os.path.dirname(os.path.realpath(__file__))}/..", | ||||
|         help="Path to Solidity project's root directory (must be fully qualified).", | ||||
|         nargs="?" | ||||
|     ) | ||||
|     return parser | ||||
| 
 | ||||
| class Counter: | ||||
|     total: int = 0 | ||||
|     passed: int = 0 | ||||
|     failed: int = 0 | ||||
| 
 | ||||
| class SolidityLSPTestSuite: # {{{ | ||||
|     test_counter = Counter() | ||||
|     assertion_counter = Counter() | ||||
|     print_assertions: bool = False | ||||
|     trace_io: bool = False | ||||
|     test_pattern: str | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         colorama.init() | ||||
|         args = create_cli_parser().parse_args() | ||||
|         self.solc_path = args.solc_path | ||||
|         self.project_root_dir = os.path.realpath(args.project_root_dir) + "/test/libsolidity/lsp" | ||||
|         self.project_root_uri = "file://" + self.project_root_dir | ||||
|         self.print_assertions = args.print_assertions | ||||
|         self.trace_io = args.trace_io | ||||
|         self.test_pattern = args.test_pattern | ||||
| 
 | ||||
|         print(f"{SGR_NOTICE}test pattern: {self.test_pattern}{SGR_RESET}") | ||||
| 
 | ||||
|     def main(self) -> int: | ||||
|         """ | ||||
|         Runs all test cases. | ||||
|         Returns 0 on success and the number of failing assertions (capped to 127) otherwise. | ||||
|         """ | ||||
|         all_tests = sorted([ | ||||
|             str(name)[5:] | ||||
|             for name in dir(SolidityLSPTestSuite) | ||||
|             if callable(getattr(SolidityLSPTestSuite, name)) and name.startswith("test_") | ||||
|         ]) | ||||
|         filtered_tests = fnmatch.filter(all_tests, self.test_pattern) | ||||
|         for method_name in filtered_tests: | ||||
|             test_fn = getattr(self, 'test_' + method_name) | ||||
|             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: | ||||
|                     test_fn(solc) | ||||
|                     self.test_counter.passed += 1 | ||||
|             except ExpectationFailed as e: | ||||
|                 self.test_counter.failed += 1 | ||||
|                 print(e) | ||||
|                 print(traceback.format_exc()) | ||||
|             except Exception as e: # pragma pylint: disable=broad-except | ||||
|                 self.test_counter.failed += 1 | ||||
|                 print(f"Unhandled exception {e.__class__.__name__} caught: {e}") | ||||
|                 print(traceback.format_exc()) | ||||
| 
 | ||||
|         print( | ||||
|             f"\n{SGR_NOTICE}Summary:{SGR_RESET}\n\n" | ||||
|             f"  Test cases: {self.test_counter.passed} passed, {self.test_counter.failed} failed\n" | ||||
|             f"  Assertions: {self.assertion_counter.passed} passed, {self.assertion_counter.failed} failed\n" | ||||
|         ) | ||||
| 
 | ||||
|         return min(max(self.test_counter.failed, self.assertion_counter.failed), 127) | ||||
| 
 | ||||
|     def setup_lsp(self, lsp: JsonRpcProcess, expose_project_root=True): | ||||
|         """ | ||||
|         Prepares the solc LSP server by calling `initialize`, | ||||
|         and `initialized` methods. | ||||
|         """ | ||||
|         params = { | ||||
|             'processId': None, | ||||
|             'rootUri': self.project_root_uri, | ||||
|             'trace': 'off', | ||||
|             'initializationOptions': {}, | ||||
|             'capabilities': { | ||||
|                 'textDocument': { | ||||
|                     'publishDiagnostics': {'relatedInformation': True} | ||||
|                 }, | ||||
|                 'workspace': { | ||||
|                     'applyEdit': True, | ||||
|                     'configuration': True, | ||||
|                     'didChangeConfiguration': {'dynamicRegistration': True}, | ||||
|                     'workspaceEdit': {'documentChanges': True}, | ||||
|                     'workspaceFolders': True | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         if expose_project_root == False: | ||||
|             params['rootUri'] = None | ||||
|         lsp.call_method('initialize', params) | ||||
|         lsp.send_notification('initialized') | ||||
| 
 | ||||
|     # {{{ helpers | ||||
|     def get_test_file_path(self, test_case_name): | ||||
|         return f"{self.project_root_dir}/{test_case_name}.sol" | ||||
| 
 | ||||
|     def get_test_file_uri(self, test_case_name): | ||||
|         return "file://" + self.get_test_file_path(test_case_name) | ||||
| 
 | ||||
|     def get_test_file_contents(self, test_case_name): | ||||
|         """ | ||||
|         Reads the file contents from disc for a given test case. | ||||
|         The `test_case_name` will be the basename of the file | ||||
|         in the test path (test/libsolidity/lsp). | ||||
|         """ | ||||
|         with open(self.get_test_file_path(test_case_name), mode="r", encoding="utf-8", newline='') as f: | ||||
|             return f.read() | ||||
| 
 | ||||
|     def require_params_for_method(self, method_name: str, message: dict) -> Any: | ||||
|         """ | ||||
|         Ensures the given RPC message does contain the | ||||
|         field 'method' with the given method name, | ||||
|         and then returns its passed params. | ||||
|         An exception is raised on expectation failures. | ||||
|         """ | ||||
|         assert message is not None | ||||
|         if 'error' in message.keys(): | ||||
|             code = message['error']["code"] | ||||
|             text = message['error']['message'] | ||||
|             raise RuntimeError(f"Error {code} received. {text}") | ||||
|         if 'method' not in message.keys(): | ||||
|             raise RuntimeError("No method received but something else.") | ||||
|         self.expect_equal(message['method'], method_name, "Ensure expected method name") | ||||
|         return message['params'] | ||||
| 
 | ||||
|     def wait_for_diagnostics(self, solc: JsonRpcProcess, count: int) -> List[dict]: | ||||
|         """ | ||||
|         Return `count` number of published diagnostic reports sorted by file URI. | ||||
|         """ | ||||
|         reports = [] | ||||
|         for _ in range(0, count): | ||||
|             message = solc.receive_message() | ||||
|             assert message is not None # This can happen if the server aborts early. | ||||
|             reports.append( | ||||
|                 self.require_params_for_method( | ||||
|                     'textDocument/publishDiagnostics', | ||||
|                     message, | ||||
|                 ) | ||||
|             ) | ||||
|         return sorted(reports, key=lambda x: x['uri']) | ||||
| 
 | ||||
|     def open_file_and_wait_for_diagnostics( | ||||
|         self, | ||||
|         solc_process: JsonRpcProcess, | ||||
|         test_case_name: str, | ||||
|         max_diagnostic_reports: int = 1 | ||||
|     ) -> List[Any]: | ||||
|         """ | ||||
|         Opens file for given test case and waits for diagnostics to be published. | ||||
|         """ | ||||
|         assert max_diagnostic_reports > 0 | ||||
|         solc_process.send_message( | ||||
|             'textDocument/didOpen', | ||||
|             { | ||||
|                 'textDocument': | ||||
|                 { | ||||
|                     'uri': self.get_test_file_uri(test_case_name), | ||||
|                     'languageId': 'Solidity', | ||||
|                     'version': 1, | ||||
|                     'text': self.get_test_file_contents(test_case_name) | ||||
|                 } | ||||
|             } | ||||
|         ) | ||||
|         return self.wait_for_diagnostics(solc_process, max_diagnostic_reports) | ||||
| 
 | ||||
|     def expect_equal(self, actual, expected, description="Equality") -> None: | ||||
|         self.assertion_counter.total += 1 | ||||
|         prefix = f"[{self.assertion_counter.total}] {SGR_ASSERT_BEGIN}{description}: " | ||||
|         diff = DeepDiff(actual, expected) | ||||
|         if len(diff) == 0: | ||||
|             self.assertion_counter.passed += 1 | ||||
|             if self.print_assertions: | ||||
|                 print(prefix + SGR_STATUS_OKAY + 'OK' + SGR_RESET) | ||||
|             return | ||||
| 
 | ||||
|         # Failed assertions are always printed. | ||||
|         self.assertion_counter.failed += 1 | ||||
|         print(prefix + SGR_STATUS_FAIL + 'FAILED' + SGR_RESET) | ||||
|         raise ExpectationFailed(actual, expected) | ||||
| 
 | ||||
|     def expect_empty_diagnostics(self, published_diagnostics: List[dict]) -> None: | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
| 
 | ||||
|     def expect_diagnostic( | ||||
|         self, | ||||
|         diagnostic, | ||||
|         code: int, | ||||
|         lineNo: int, | ||||
|         startEndColumns: Tuple[int, int] | ||||
|     ): | ||||
|         assert len(startEndColumns) == 2 | ||||
|         [startColumn, endColumn] = startEndColumns | ||||
|         self.expect_equal(diagnostic['code'], code, f'diagnostic: {code}') | ||||
|         self.expect_equal( | ||||
|             diagnostic['range'], | ||||
|             { | ||||
|                 'start': {'character': startColumn, 'line': lineNo}, | ||||
|                 'end': {'character': endColumn, 'line': lineNo} | ||||
|             }, | ||||
|             "diagnostic: check range" | ||||
|         ) | ||||
|     # }}} | ||||
| 
 | ||||
|     # {{{ actual tests | ||||
|     def test_publish_diagnostics_warnings(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc) | ||||
|         TEST_NAME = 'publish_diagnostics_1' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) | ||||
| 
 | ||||
|         self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") | ||||
|         report = published_diagnostics[0] | ||||
| 
 | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         diagnostics = report['diagnostics'] | ||||
| 
 | ||||
|         self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") | ||||
|         self.expect_diagnostic(diagnostics[0], code=6321, lineNo=13, startEndColumns=(44, 48)) | ||||
|         self.expect_diagnostic(diagnostics[1], code=2072, lineNo= 7, startEndColumns=( 8, 19)) | ||||
|         self.expect_diagnostic(diagnostics[2], code=2072, lineNo=15, startEndColumns=( 8, 20)) | ||||
| 
 | ||||
|     def test_publish_diagnostics_errors(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc) | ||||
|         TEST_NAME = 'publish_diagnostics_2' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) | ||||
| 
 | ||||
|         self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") | ||||
|         report = published_diagnostics[0] | ||||
| 
 | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         diagnostics = report['diagnostics'] | ||||
| 
 | ||||
|         self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") | ||||
|         self.expect_diagnostic(diagnostics[0], code=9574, lineNo= 7, startEndColumns=( 8, 21)) | ||||
|         self.expect_diagnostic(diagnostics[1], code=6777, lineNo= 8, startEndColumns=( 8, 15)) | ||||
|         self.expect_diagnostic(diagnostics[2], code=6160, lineNo=18, startEndColumns=(15, 36)) | ||||
| 
 | ||||
|     def test_publish_diagnostics_errors_multiline(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc) | ||||
|         TEST_NAME = 'publish_diagnostics_3' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) | ||||
| 
 | ||||
|         self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") | ||||
|         report = published_diagnostics[0] | ||||
| 
 | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         diagnostics = report['diagnostics'] | ||||
| 
 | ||||
|         self.expect_equal(len(diagnostics), 1, "3 diagnostic messages") | ||||
|         self.expect_equal(diagnostics[0]['code'], 3656, "diagnostic: check code") | ||||
|         self.expect_equal( | ||||
|             diagnostics[0]['range'], | ||||
|             { | ||||
|                 'end': {'character': 1, 'line': 9}, | ||||
|                 'start': {'character': 0, 'line': 7} | ||||
|             }, | ||||
|             "diagnostic: check range" | ||||
|         ) | ||||
| 
 | ||||
|     def test_textDocument_didOpen_with_relative_import(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc) | ||||
|         TEST_NAME = 'didOpen_with_import' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME, 2) | ||||
| 
 | ||||
|         self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") | ||||
| 
 | ||||
|         # primary file: | ||||
|         report = published_diagnostics[0] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         self.expect_equal(len(report['diagnostics']), 0, "no diagnostics") | ||||
| 
 | ||||
|         # imported file (./lib.sol): | ||||
|         report = published_diagnostics[1] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") | ||||
|         self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") | ||||
|         self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) | ||||
| 
 | ||||
|     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) | ||||
|         self.open_file_and_wait_for_diagnostics(solc, 'lib', 2) | ||||
|         solc.send_message( | ||||
|             'textDocument/didChange', | ||||
|             { | ||||
|                 'textDocument': | ||||
|                 { | ||||
|                     'uri': self.get_test_file_uri('lib') | ||||
|                 }, | ||||
|                 'contentChanges': | ||||
|                 [ | ||||
|                     { | ||||
|                         'range': { | ||||
|                             'start': { 'line':  5, 'character': 0 }, | ||||
|                             'end':   { 'line': 10, 'character': 0 } | ||||
|                         }, | ||||
|                         'text': "" # deleting function `add` | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 2) | ||||
|         self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") | ||||
| 
 | ||||
|         # Main file now contains a new diagnostic | ||||
|         report = published_diagnostics[0] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri('didOpen_with_import')) | ||||
|         diagnostics = report['diagnostics'] | ||||
|         self.expect_equal(len(diagnostics), 1, "now, no diagnostics") | ||||
|         self.expect_diagnostic(diagnostics[0], code=9582, lineNo=9, startEndColumns=(15, 22)) | ||||
| 
 | ||||
|         # The modified file retains the same diagnostics. | ||||
|         report = published_diagnostics[1] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri('lib')) | ||||
|         self.expect_equal(len(report['diagnostics']), 0) | ||||
|         # The warning went away because the compiler aborts further processing after the error. | ||||
| 
 | ||||
|     def test_textDocument_didOpen_with_relative_import_without_project_url(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc, expose_project_root=False) | ||||
|         TEST_NAME = 'didOpen_with_import' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME, 2) | ||||
|         self.verify_didOpen_with_import_diagnostics(published_diagnostics) | ||||
| 
 | ||||
|     def verify_didOpen_with_import_diagnostics( | ||||
|         self, | ||||
|         published_diagnostics: List[Any], | ||||
|         main_file_name='didOpen_with_import' | ||||
|     ): | ||||
|         self.expect_equal(len(published_diagnostics), 2, "Diagnostic reports for 2 files") | ||||
| 
 | ||||
|         # primary file: | ||||
|         report = published_diagnostics[0] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(main_file_name), "Correct file URI") | ||||
|         self.expect_equal(len(report['diagnostics']), 0, "one diagnostic") | ||||
| 
 | ||||
|         # imported file (./lib.sol): | ||||
|         report = published_diagnostics[1] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri('lib'), "Correct file URI") | ||||
|         self.expect_equal(len(report['diagnostics']), 1, "one diagnostic") | ||||
|         self.expect_diagnostic(report['diagnostics'][0], code=2072, lineNo=12, startEndColumns=(8, 19)) | ||||
| 
 | ||||
|     def test_textDocument_didChange_updates_diagnostics(self, solc: JsonRpcProcess) -> None: | ||||
|         self.setup_lsp(solc) | ||||
|         TEST_NAME = 'publish_diagnostics_1' | ||||
|         published_diagnostics = self.open_file_and_wait_for_diagnostics(solc, TEST_NAME) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "One published_diagnostics message") | ||||
|         report = published_diagnostics[0] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         diagnostics = report['diagnostics'] | ||||
|         self.expect_equal(len(diagnostics), 3, "3 diagnostic messages") | ||||
|         self.expect_diagnostic(diagnostics[0], code=6321, lineNo=13, startEndColumns=(44, 48)) | ||||
|         self.expect_diagnostic(diagnostics[1], code=2072, lineNo= 7, startEndColumns=( 8, 19)) | ||||
|         self.expect_diagnostic(diagnostics[2], code=2072, lineNo=15, startEndColumns=( 8, 20)) | ||||
| 
 | ||||
|         solc.send_message( | ||||
|             'textDocument/didChange', | ||||
|             { | ||||
|                 'textDocument': { | ||||
|                     'uri': self.get_test_file_uri(TEST_NAME) | ||||
|                 }, | ||||
|                 'contentChanges': [ | ||||
|                     { | ||||
|                         'range': { | ||||
|                             'start': { 'line': 7, 'character': 1 }, | ||||
|                             'end': {   'line': 8, 'character': 1 } | ||||
|                         }, | ||||
|                         'text': "" | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1) | ||||
|         report = published_diagnostics[0] | ||||
|         self.expect_equal(report['uri'], self.get_test_file_uri(TEST_NAME), "Correct file URI") | ||||
|         diagnostics = report['diagnostics'] | ||||
|         self.expect_equal(len(diagnostics), 2) | ||||
|         self.expect_diagnostic(diagnostics[0], code=6321, lineNo=12, startEndColumns=(44, 48)) | ||||
|         self.expect_diagnostic(diagnostics[1], code=2072, lineNo=14, startEndColumns=( 8, 20)) | ||||
| 
 | ||||
|     def test_textDocument_didChange_delete_line_and_close(self, solc: JsonRpcProcess) -> None: | ||||
|         # Reuse this test to prepare and ensure it is as expected | ||||
|         self.test_textDocument_didOpen_with_relative_import(solc) | ||||
|         self.open_file_and_wait_for_diagnostics(solc, 'lib', 2) | ||||
|         # lib.sol: Fix the unused variable message by removing it. | ||||
|         solc.send_message( | ||||
|             'textDocument/didChange', | ||||
|             { | ||||
|                 'textDocument': | ||||
|                 { | ||||
|                     'uri': self.get_test_file_uri('lib') | ||||
|                 }, | ||||
|                 'contentChanges': # delete the in-body statement: `uint unused;` | ||||
|                 [ | ||||
|                     { | ||||
|                         'range': | ||||
|                         { | ||||
|                             'start': { 'line': 12, 'character': 1 }, | ||||
|                             'end':   { 'line': 13, 'character': 1 } | ||||
|                         }, | ||||
|                         'text': "" | ||||
|                     } | ||||
|                 ] | ||||
|             } | ||||
|         ) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 2) | ||||
|         self.expect_equal(len(published_diagnostics), 2, "published diagnostics count") | ||||
|         report1 = published_diagnostics[0] | ||||
|         self.expect_equal(report1['uri'], self.get_test_file_uri('didOpen_with_import'), "Correct file URI") | ||||
|         self.expect_equal(len(report1['diagnostics']), 0, "no diagnostics in didOpen_with_import.sol") | ||||
|         report2 = published_diagnostics[1] | ||||
|         self.expect_equal(report2['uri'], self.get_test_file_uri('lib'), "Correct file URI") | ||||
|         self.expect_equal(len(report2['diagnostics']), 0, "no diagnostics in lib.sol") | ||||
| 
 | ||||
|         # Now close the file and expect the warning to re-appear | ||||
|         solc.send_message( | ||||
|             'textDocument/didClose', | ||||
|             { 'textDocument': { 'uri': self.get_test_file_uri('lib') }} | ||||
|         ) | ||||
| 
 | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 2) | ||||
|         self.verify_didOpen_with_import_diagnostics(published_diagnostics) | ||||
| 
 | ||||
|     def test_textDocument_opening_two_new_files_edit_and_close(self, solc: JsonRpcProcess) -> None: | ||||
|         """ | ||||
|         Open two new files A and B, let A import B, expect no error, | ||||
|         then close B and now expect the error of file B not being found. | ||||
|         """ | ||||
| 
 | ||||
|         self.setup_lsp(solc) | ||||
|         FILE_A_URI = 'file:///a.sol' | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_A_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': ''.join([ | ||||
|                     '// SPDX-License-Identifier: UNLICENSED\n', | ||||
|                     'pragma solidity >=0.8.0;\n', | ||||
|                 ]) | ||||
|             } | ||||
|         }) | ||||
|         reports = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(reports), 1, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
| 
 | ||||
|         FILE_B_URI = 'file:///b.sol' | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_B_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': ''.join([ | ||||
|                     '// SPDX-License-Identifier: UNLICENSED\n', | ||||
|                     'pragma solidity >=0.8.0;\n', | ||||
|                 ]) | ||||
|             } | ||||
|         }) | ||||
|         reports = self.wait_for_diagnostics(solc, 2) | ||||
|         self.expect_equal(len(reports), 2, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
|         self.expect_equal(len(reports[1]['diagnostics']), 0, "should not contain diagnostics") | ||||
| 
 | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_A_URI | ||||
|             }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 2, 'character': 0 }, | ||||
|                         'end': { 'line': 2, 'character': 0 } | ||||
|                     }, | ||||
|                     'text': 'import "./b.sol";\n' | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         reports = self.wait_for_diagnostics(solc, 2) | ||||
|         self.expect_equal(len(reports), 2, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
|         self.expect_equal(len(reports[1]['diagnostics']), 0, "should not contain diagnostics") | ||||
| 
 | ||||
|         solc.send_message( | ||||
|             'textDocument/didClose', | ||||
|             { 'textDocument': { 'uri': FILE_B_URI }} | ||||
|         ) | ||||
|         # We only get one diagnostics message since the diagnostics for b.sol was empty. | ||||
|         reports = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(reports), 1, "one publish diagnostics notification") | ||||
|         self.expect_diagnostic(reports[0]['diagnostics'][0], 6275, 2, (0, 17)) # a.sol: File B not found | ||||
|         self.expect_equal(reports[0]['uri'], FILE_A_URI, "Correct uri") | ||||
| 
 | ||||
|     def test_textDocument_closing_virtual_file_removes_imported_real_file(self, solc: JsonRpcProcess) -> None: | ||||
|         """ | ||||
|         We open a virtual file that imports a real file with a warning. | ||||
|         Once we close the virtual file, the warning is removed from the diagnostics, | ||||
|         since the real file is not considered part of the project anymore. | ||||
|         """ | ||||
| 
 | ||||
|         self.setup_lsp(solc) | ||||
|         FILE_A_URI = f'file://{self.project_root_dir}/a.sol' | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_A_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': | ||||
|                     '// SPDX-License-Identifier: UNLICENSED\n' | ||||
|                     'pragma solidity >=0.8.0;\n' | ||||
|                     'import "./lib.sol";\n' | ||||
|             } | ||||
|         }) | ||||
|         reports = self.wait_for_diagnostics(solc, 2) | ||||
|         self.expect_equal(len(reports), 2, '') | ||||
|         self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
|         self.expect_diagnostic(reports[1]['diagnostics'][0], 2072, 12, (8, 19)) # unused variable in lib.sol | ||||
| 
 | ||||
|         # Now close the file and expect the warning for lib.sol to be removed | ||||
|         solc.send_message( | ||||
|             'textDocument/didClose', | ||||
|             { 'textDocument': { 'uri': FILE_A_URI }} | ||||
|         ) | ||||
|         reports = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(reports), 1, '') | ||||
|         self.expect_equal(reports[0]['uri'], f'file://{self.project_root_dir}/lib.sol', "") | ||||
|         self.expect_equal(len(reports[0]['diagnostics']), 0, "should not contain diagnostics") | ||||
| 
 | ||||
| 
 | ||||
|     def test_textDocument_didChange_at_eol(self, solc: JsonRpcProcess) -> None: | ||||
|         """ | ||||
|         Append at one line and insert a new one below. | ||||
|         """ | ||||
|         self.setup_lsp(solc) | ||||
|         FILE_NAME = 'didChange_template' | ||||
|         FILE_URI = self.get_test_file_uri(FILE_NAME) | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': self.get_test_file_contents(FILE_NAME) | ||||
|             } | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "no diagnostics") | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_URI | ||||
|             }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 6, 'character': 0 }, | ||||
|                         'end': { 'line': 6, 'character': 0 } | ||||
|                     }, | ||||
|                     'text': " f" | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         report2 = published_diagnostics[0] | ||||
|         self.expect_equal(report2['uri'], FILE_URI, "Correct file URI") | ||||
|         self.expect_equal(len(report2['diagnostics']), 1, "one diagnostic") | ||||
|         self.expect_diagnostic(report2['diagnostics'][0], 7858, 6, (1, 2)) | ||||
| 
 | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { 'uri': FILE_URI }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 6, 'character': 2 }, | ||||
|                         'end': { 'line': 6, 'character': 2 } | ||||
|                     }, | ||||
|                     'text': 'unction f() public {}' | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         report3 = published_diagnostics[0] | ||||
|         self.expect_equal(report3['uri'], FILE_URI, "Correct file URI") | ||||
|         self.expect_equal(len(report3['diagnostics']), 1, "one diagnostic") | ||||
|         self.expect_diagnostic(report3['diagnostics'][0], 4126, 6, (1, 23)) | ||||
| 
 | ||||
|     def test_textDocument_didChange_empty_file(self, solc: JsonRpcProcess) -> None: | ||||
|         """ | ||||
|         Starts with an empty file and changes it to look like | ||||
|         the didOpen_with_import test case. Then we can use | ||||
|         the same verification calls to ensure it worked as expected. | ||||
|         """ | ||||
|         # This FILE_NAME must be alphabetically before lib.sol to not over-complify | ||||
|         # the test logic in verify_didOpen_with_import_diagnostics. | ||||
|         FILE_NAME = 'a_new_file' | ||||
|         FILE_URI = self.get_test_file_uri(FILE_NAME) | ||||
|         self.setup_lsp(solc) | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': '' | ||||
|             } | ||||
|         }) | ||||
|         reports = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(reports), 1) | ||||
|         report = reports[0] | ||||
|         published_diagnostics = report['diagnostics'] | ||||
|         self.expect_equal(len(published_diagnostics), 2) | ||||
|         self.expect_diagnostic(published_diagnostics[0], code=1878, lineNo=0, startEndColumns=(0, 0)) | ||||
|         self.expect_diagnostic(published_diagnostics[1], code=3420, lineNo=0, startEndColumns=(0, 0)) | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { | ||||
|                 'uri': self.get_test_file_uri('a_new_file') | ||||
|             }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 0, 'character': 0 }, | ||||
|                         'end': { 'line': 0, 'character': 0 } | ||||
|                     }, | ||||
|                     'text': self.get_test_file_contents('didOpen_with_import') | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 2) | ||||
|         self.verify_didOpen_with_import_diagnostics(published_diagnostics, 'a_new_file') | ||||
| 
 | ||||
|     def test_textDocument_didChange_multi_line(self, solc: JsonRpcProcess) -> None: | ||||
|         """ | ||||
|         Starts with an empty file and changes it to multiple times, changing | ||||
|         content across lines. | ||||
|         """ | ||||
|         self.setup_lsp(solc) | ||||
|         FILE_NAME = 'didChange_template' | ||||
|         FILE_URI = self.get_test_file_uri(FILE_NAME) | ||||
|         solc.send_message('textDocument/didOpen', { | ||||
|             'textDocument': { | ||||
|                 'uri': FILE_URI, | ||||
|                 'languageId': 'Solidity', | ||||
|                 'version': 1, | ||||
|                 'text': self.get_test_file_contents(FILE_NAME) | ||||
|             } | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         self.expect_equal(len(published_diagnostics[0]['diagnostics']), 0, "no diagnostics") | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { 'uri': FILE_URI }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 3, 'character': 3 }, | ||||
|                         'end': { 'line': 4, 'character': 1 } | ||||
|                     }, | ||||
|                     'text': "tract D {\n\n  uint x\n = -1; \n " | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         report2 = published_diagnostics[0] | ||||
|         self.expect_equal(report2['uri'], FILE_URI, "Correct file URI") | ||||
|         self.expect_equal(len(report2['diagnostics']), 1, "one diagnostic") | ||||
|         self.expect_diagnostic(report2['diagnostics'][0], 7407, 6, (3, 5)) | ||||
| 
 | ||||
|         # Now we are changing the part "x\n = -" of "uint x\n = -1;" | ||||
|         solc.send_message('textDocument/didChange', { | ||||
|             'textDocument': { 'uri': FILE_URI }, | ||||
|             'contentChanges': [ | ||||
|                 { | ||||
|                     'range': { | ||||
|                         'start': { 'line': 5, 'character': 7 }, | ||||
|                         'end': { 'line': 6, 'character': 4 } | ||||
|                     }, | ||||
|                     'text': "y\n = [\nuint(1),\n3,4]+" | ||||
|                 } | ||||
|             ] | ||||
|         }) | ||||
|         published_diagnostics = self.wait_for_diagnostics(solc, 1) | ||||
|         self.expect_equal(len(published_diagnostics), 1, "one publish diagnostics notification") | ||||
|         report3 = published_diagnostics[0] | ||||
|         self.expect_equal(report3['uri'], FILE_URI, "Correct file URI") | ||||
|         self.expect_equal(len(report3['diagnostics']), 2, "two diagnostics") | ||||
|         diagnostic = report3['diagnostics'][0] | ||||
|         self.expect_equal(diagnostic['code'], 2271, 'diagnostic: 2271') | ||||
|         # check multi-line error code | ||||
|         self.expect_equal( | ||||
|             diagnostic['range'], | ||||
|             { | ||||
|                 'end': {'character': 6, 'line': 8}, | ||||
|                 'start': {'character': 3, 'line': 6} | ||||
|             }, | ||||
|             "diagnostic: check range" | ||||
|         ) | ||||
|         diagnostic = report3['diagnostics'][1] | ||||
|         self.expect_equal(diagnostic['code'], 7407, 'diagnostic: 7407') | ||||
|         # check multi-line error code | ||||
|         self.expect_equal( | ||||
|             diagnostic['range'], | ||||
|             { | ||||
|                 'end': {'character': 6, 'line': 8}, | ||||
|                 'start': {'character': 3, 'line': 6} | ||||
|             }, | ||||
|             "diagnostic: check range" | ||||
|         ) | ||||
| 
 | ||||
|     # }}} | ||||
|     # }}} | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     suite = SolidityLSPTestSuite() | ||||
|     exit_code = suite.main() | ||||
|     exit(exit_code) | ||||
| @ -158,7 +158,7 @@ BOOST_AUTO_TEST_CASE(multiple_input_modes) | ||||
| 	}; | ||||
| 	string expectedMessage = | ||||
| 		"The following options are mutually exclusive: " | ||||
| 		"--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast. " | ||||
| 		"--help, --license, --version, --standard-json, --link, --assemble, --strict-assembly, --yul, --import-ast, --lsp. " | ||||
| 		"Select at most one."; | ||||
| 
 | ||||
| 	for (string const& mode1: inputModeOptions) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user