/* This file is part of solidity. solidity is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. solidity is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with solidity. If not, see . */ // SPDX-License-Identifier: GPL-3.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace std::placeholders; using namespace solidity::lsp; using namespace solidity::langutil; using namespace solidity::frontend; namespace { void log(string const& _message) { #if 0 static ofstream logFile("/tmp/solc.lsp.log", std::ios::app); logFile << _message << endl; #else (void)_message; #endif } struct MarkdownBuilder { std::stringstream result; MarkdownBuilder& code(std::string const& _code) { // TODO: Use solidity as language Id as soon as possible. auto constexpr SolidityLanguageId = "javascript"; result << "```" << SolidityLanguageId << '\n' << _code << "\n```\n\n"; return *this; } MarkdownBuilder& text(std::string const& _text) { if (_text.empty()) { result << _text << '\n'; if (_text.back() != '\n') // We want double-LF to ensure constructing a paragraph. result << '\n'; } return *this; } }; 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(int _startLine, int _startColumn, int _endLine, int _endColumn) { Json::Value json; json["start"] = toJson({_startLine, _startColumn}); json["end"] = toJson({_endLine, _endColumn}); return json; } 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; } return 1; } vector allAnnotatedDeclarations(Identifier const* _identifier) { vector output; output.push_back(_identifier->annotation().referencedDeclaration); output += _identifier->annotation().candidateDeclarations; return output; } Json::Value semanticTokensLegend() { Json::Value legend = Json::objectValue; // NOTE! The (alphabetical) order and items must match exactly the items of // their respective enum class members. Json::Value tokenTypes = Json::arrayValue; tokenTypes.append("class"); tokenTypes.append("comment"); tokenTypes.append("enum"); tokenTypes.append("enumMember"); tokenTypes.append("event"); tokenTypes.append("function"); tokenTypes.append("interface"); tokenTypes.append("keyword"); tokenTypes.append("macro"); tokenTypes.append("method"); tokenTypes.append("modifier"); tokenTypes.append("number"); tokenTypes.append("operator"); tokenTypes.append("parameter"); tokenTypes.append("property"); tokenTypes.append("string"); tokenTypes.append("struct"); tokenTypes.append("type"); tokenTypes.append("typeParameter"); tokenTypes.append("variable"); legend["tokenTypes"] = tokenTypes; Json::Value tokenModifiers = Json::arrayValue; tokenModifiers.append("abstract"); tokenModifiers.append("declaration"); tokenModifiers.append("definition"); tokenModifiers.append("deprecated"); tokenModifiers.append("documentation"); tokenModifiers.append("modification"); tokenModifiers.append("readonly"); legend["tokenModifiers"] = tokenModifiers; return legend; } } LanguageServer::LanguageServer(unique_ptr _transport): m_client{move(_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) { terminate(); }}, {"initialize", bind(&LanguageServer::handleInitialize, this, _1, _2)}, {"initialized", [](auto, auto) {} }, {"shutdown", [this](auto, auto) { m_shutdownRequested = true; }}, {"textDocument/definition", [this](auto _id, auto _args) { handleGotoDefinition(_id, _args); }}, {"textDocument/didChange", bind(&LanguageServer::handleTextDocumentDidChange, this, _1, _2)}, {"textDocument/didClose", [](auto, auto) {/*nothing for now*/}}, {"textDocument/didOpen", bind(&LanguageServer::handleTextDocumentDidOpen, this, _1, _2)}, {"textDocument/documentHighlight", bind(&LanguageServer::handleTextDocumentHighlight, this, _1, _2)}, {"textDocument/hover", bind(&LanguageServer::handleTextDocumentHover, this, _1, _2)}, {"textDocument/implementation", [this](auto _id, auto _args) { handleGotoDefinition(_id, _args); }}, {"textDocument/references", bind(&LanguageServer::handleTextDocumentReferences, this, _1, _2)}, {"textDocument/semanticTokens/full", bind(&LanguageServer::semanticTokensFull, this, _1, _2)}, {"workspace/didChangeConfiguration", bind(&LanguageServer::handleWorkspaceDidChangeConfiguration, this, _1, _2)}, }, m_fileReader{"/"}, m_compilerStack{bind(&FileReader::readFile, ref(m_fileReader), _1, _2)} { } DocumentPosition LanguageServer::extractDocumentPosition(Json::Value const& _json) const { DocumentPosition dpos{}; dpos.path = _json["textDocument"]["uri"].asString(); dpos.position.line = _json["position"]["line"].asInt(); dpos.position.column = _json["position"]["character"].asInt(); return dpos; } Json::Value LanguageServer::toRange(SourceLocation const& _location) const { solAssert(_location.sourceName, ""); CharStream const& stream = m_compilerStack.charStream(*_location.sourceName); auto const [startLine, startColumn] = stream.translatePositionToLineColumn(_location.start); auto const [endLine, endColumn] = stream.translatePositionToLineColumn(_location.end); return toJsonRange(startLine, startColumn, endLine, endColumn); } Json::Value LanguageServer::toJson(SourceLocation const& _location) const { solAssert(_location.sourceName); Json::Value item = Json::objectValue; item["uri"] = m_fileMappings.at(*_location.sourceName); item["range"] = toRange(_location); return item; } string LanguageServer::clientPathToSourceUnitName(string const& _path) const { return m_fileReader.cliPathToSourceUnitName(_path); } bool LanguageServer::clientPathSourceKnown(string const& _path) const { return m_fileReader.sourceCodes().count(clientPathToSourceUnitName(_path)); } void LanguageServer::changeConfiguration(Json::Value const& _settings) { m_settingsObject = _settings; } bool LanguageServer::compile(string const& _path) { // TODO: optimize! do not recompile if nothing has changed (file(s) not flagged dirty). if (!clientPathSourceKnown(_path)) return false; m_compilerStack.reset(false); m_compilerStack.setSources(m_fileReader.sourceCodes()); m_compilerStack.compile(CompilerStack::State::AnalysisPerformed); return true; } void LanguageServer::compileSourceAndReport(string const& _path) { compile(_path); Json::Value params; params["uri"] = _path; params["diagnostics"] = Json::arrayValue; for (shared_ptr const& error: m_compilerStack.errors()) { SourceReferenceExtractor::Message const message = SourceReferenceExtractor::extract(m_compilerStack, *error); Json::Value jsonDiag; jsonDiag["source"] = "solc"; jsonDiag["severity"] = toDiagnosticSeverity(error->type()); jsonDiag["message"] = message.primary.message; jsonDiag["range"] = toJsonRange( message.primary.position.line, message.primary.startColumn, message.primary.position.line, message.primary.endColumn ); if (message.errorId.has_value()) jsonDiag["code"] = Json::UInt64{message.errorId.value().error}; for (SourceReference const& secondary: message.secondary) { Json::Value jsonRelated; jsonRelated["message"] = secondary.message; // TODO translate back? jsonRelated["location"]["uri"] = secondary.sourceName; jsonRelated["location"]["range"] = toJsonRange( secondary.position.line, secondary.startColumn, secondary.position.line, secondary.endColumn ); jsonDiag["relatedInformation"].append(jsonRelated); } params["diagnostics"].append(jsonDiag); } m_client->notify("textDocument/publishDiagnostics", params); } ASTNode const* LanguageServer::requestASTNode(DocumentPosition _filePos) { if (m_compilerStack.state() < CompilerStack::AnalysisPerformed) return nullptr; if (!clientPathSourceKnown(_filePos.path)) return nullptr; string sourceUnitName = clientPathToSourceUnitName(_filePos.path); optional sourcePos = m_compilerStack.charStream(sourceUnitName) .translateLineColumnToPosition(_filePos.position.line, _filePos.position.column); if (!sourcePos.has_value()) return nullptr; return locateInnermostASTNode(*sourcePos, m_compilerStack.ast(sourceUnitName)); } optional LanguageServer::declarationPosition(Declaration const* _declaration) { if (!_declaration) return nullopt; if (_declaration->nameLocation().isValid()) return _declaration->nameLocation(); if (_declaration->location().isValid()) return _declaration->location(); return nullopt; } vector LanguageServer::findAllReferences( Declaration const* _declaration, string const& _sourceIdentifierName, SourceUnit const& _sourceUnit ) { vector output; for (DocumentHighlight& highlight: ReferenceCollector::collect(_declaration, _sourceUnit, _sourceIdentifierName)) output.emplace_back(move(highlight.location)); return output; } void LanguageServer::findAllReferences( Declaration const* _declaration, string const& _sourceIdentifierName, SourceUnit const& _sourceUnit, vector& _output ) { for (DocumentHighlight& highlight: ReferenceCollector::collect(_declaration, _sourceUnit, _sourceIdentifierName)) _output.emplace_back(move(highlight.location)); } vector LanguageServer::references(DocumentPosition _documentPosition) { ASTNode const* sourceNode = requestASTNode(_documentPosition); if (!sourceNode) return {}; SourceUnit const& sourceUnit = m_compilerStack.ast(clientPathToSourceUnitName(_documentPosition.path)); vector output; if (auto const* identifier = dynamic_cast(sourceNode)) { for (auto const* declaration: allAnnotatedDeclarations(identifier)) output += findAllReferences(declaration, declaration->name(), sourceUnit); } else if (auto const* declaration = dynamic_cast(sourceNode)) { output += findAllReferences(declaration, declaration->name(), sourceUnit); } else if (auto const* memberAccess = dynamic_cast(sourceNode)) { if (Declaration const* decl = memberAccess->annotation().referencedDeclaration) output += findAllReferences(decl, memberAccess->memberName(), sourceUnit); } return output; } vector LanguageServer::semanticHighlight(ASTNode const* _sourceNode, string const& _path) { ASTNode const* sourceNode = _sourceNode; // TODO if (!sourceNode) return {}; SourceUnit const& sourceUnit = m_compilerStack.ast(clientPathToSourceUnitName(_path)); vector output; if (auto const* declaration = dynamic_cast(sourceNode)) { output += ReferenceCollector::collect(declaration, sourceUnit, declaration->name()); } else if (auto const* identifier = dynamic_cast(sourceNode)) { for (auto const* declaration: allAnnotatedDeclarations(identifier)) output += ReferenceCollector::collect(declaration, sourceUnit, identifier->name()); } else if (auto const* identifierPath = dynamic_cast(sourceNode)) { solAssert(!identifierPath->path().empty(), ""); output += ReferenceCollector::collect(identifierPath->annotation().referencedDeclaration, sourceUnit, identifierPath->path().back()); } else if (auto const* memberAccess = dynamic_cast(sourceNode)) { Type const* type = memberAccess->expression().annotation().type; if (auto const* ttype = dynamic_cast(type)) { auto const memberName = memberAccess->memberName(); if (auto const* enumType = dynamic_cast(ttype->actualType())) { // find the definition vector output; for (ASTPointer const& enumMember: enumType->enumDefinition().members()) if (enumMember->name() == memberName) output += ReferenceCollector::collect(enumMember.get(), sourceUnit, enumMember->name()); // TODO: find uses of the enum value } } else if (auto const* structType = dynamic_cast(type)) { (void) structType; // TODO // TODO: highlight all struct member occurrences. // memberAccess->memberName() // structType-> } else { // TODO: EnumType, ... //log("semanticHighlight: member type is: "s + (type ? typeid(*type).name() : "NULL")); } } return output; } bool LanguageServer::run() { while (!m_exitRequested && !m_client->closed()) { optional const jsonMessage = m_client->receive(); if (!jsonMessage) continue; try { string const methodName = (*jsonMessage)["method"].asString(); MessageID const id = (*jsonMessage)["id"]; if (auto handler = valueOrDefault(m_handlers, methodName)) handler(id, (*jsonMessage)["params"]); else m_client->error(id, ErrorCode::MethodNotFound, "Unknown method " + methodName); } catch (exception const& e) { log("Unhandled exception caught when handling message. "s + e.what()); } } return m_shutdownRequested; } void LanguageServer::handleInitialize(MessageID _id, Json::Value const& _args) { // 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(); else if (Json::Value rootPath = _args["rootPath"]) rootPath = rootPath.asString(); //log("root path: " + rootPath); m_fileReader.setBasePath(boost::filesystem::path(rootPath)); if (_args["initializationOptions"].isObject()) changeConfiguration(_args["initializationOptions"]); Json::Value replyArgs; replyArgs["serverInfo"]["name"] = "solc"; replyArgs["serverInfo"]["version"] = string(VersionNumber); replyArgs["hoverProvider"] = true; replyArgs["capabilities"]["hoverProvider"] = true; replyArgs["capabilities"]["textDocumentSync"]["openClose"] = true; replyArgs["capabilities"]["textDocumentSync"]["change"] = 2; // 0=none, 1=full, 2=incremental replyArgs["capabilities"]["definitionProvider"] = true; replyArgs["capabilities"]["implementationProvider"] = true; replyArgs["capabilities"]["documentHighlightProvider"] = true; replyArgs["capabilities"]["referencesProvider"] = true; replyArgs["capabilities"]["semanticTokensProvider"]["legend"] = semanticTokensLegend(); replyArgs["capabilities"]["semanticTokensProvider"]["range"] = true; replyArgs["capabilities"]["semanticTokensProvider"]["full"] = true; // XOR requests.full.delta = true m_client->reply(_id, replyArgs); } void LanguageServer::handleWorkspaceDidChangeConfiguration(MessageID, Json::Value const& _args) { if (_args["settings"].isObject()) changeConfiguration(_args["settings"]); } void LanguageServer::handleTextDocumentDidOpen(MessageID /*_id*/, Json::Value const& _args) { if (!_args["textDocument"]) return; auto const text = _args["textDocument"]["text"].asString(); auto uri = _args["textDocument"]["uri"].asString(); m_fileMappings[clientPathToSourceUnitName(uri)] = uri; m_fileReader.setSource(uri, text); compileSourceAndReport(uri); } void LanguageServer::handleTextDocumentDidChange(MessageID /*_id*/, Json::Value const& _args) { auto const uri = _args["textDocument"]["uri"].asString(); auto const contentChanges = _args["contentChanges"]; for (Json::Value jsonContentChange: contentChanges) { if (!jsonContentChange.isObject()) // Protocol error, will only happen on broken clients, so silently ignore it. continue; if (!clientPathSourceKnown(uri)) // should be an error as well continue; string text = jsonContentChange["text"].asString(); if (!jsonContentChange["range"].isObject()) // full content update { m_fileReader.setSource(uri, move(text)); continue; } Json::Value const jsonRange = jsonContentChange["range"]; // TODO could use a general helper to read line/characer json objects into int pairs or whateveer int const startLine = jsonRange["start"]["line"].asInt(); int const startColumn = jsonRange["start"]["character"].asInt(); int const endLine = jsonRange["end"]["line"].asInt(); int const endColumn = jsonRange["end"]["character"].asInt(); string buffer = m_fileReader.sourceCodes().at(clientPathToSourceUnitName(uri)); optional const startOpt = CharStream::translateLineColumnToPosition(buffer, startLine, startColumn); optional const endOpt = CharStream::translateLineColumnToPosition(buffer, endLine, endColumn); if (!startOpt || !endOpt) continue; size_t const start = static_cast(startOpt.value()); size_t const count = static_cast(endOpt.value()) - start; // TODO: maybe off-by-1 bug? +1 missing? buffer.replace(start, count, move(text)); m_fileReader.setSource(uri, move(buffer)); } if (!contentChanges.empty()) compileSourceAndReport(uri); } void LanguageServer::handleGotoDefinition(MessageID _id, Json::Value const& _args) { ASTNode const* sourceNode = requestASTNode(extractDocumentPosition(_args)); vector locations; if (auto const* identifier = dynamic_cast(sourceNode)) { for (auto const* declaration: allAnnotatedDeclarations(identifier)) if (auto location = declarationPosition(declaration); location.has_value()) locations.emplace_back(move(location.value())); } else if (auto const* identifierPath = dynamic_cast(sourceNode)) { if (auto const* declaration = identifierPath->annotation().referencedDeclaration) if (auto location = declarationPosition(declaration); location.has_value()) locations.emplace_back(move(location.value())); } else if (auto const* memberAccess = dynamic_cast(sourceNode)) { auto const location = declarationPosition(memberAccess->annotation().referencedDeclaration); if (location.has_value()) locations.emplace_back(location.value()); } else if (auto const* importDirective = dynamic_cast(sourceNode)) { auto const& path = *importDirective->annotation().absolutePath; if (m_fileReader.sourceCodes().count(path)) locations.emplace_back(SourceLocation{0, 0, make_shared(path)}); } else if (auto const* declaration = dynamic_cast(sourceNode)) { if (auto location = declarationPosition(declaration); location.has_value()) locations.emplace_back(move(location.value())); } else if (sourceNode) { log(fmt::format("Could not infer def of {}", typeid(*sourceNode).name())); } Json::Value reply = Json::arrayValue; for (SourceLocation const& location: locations) reply.append(toJson(location)); m_client->reply(_id, reply); } string LanguageServer::symbolHoverInformation(ASTNode const* _sourceNode) { MarkdownBuilder markdown{}; // Try getting the type definition of the underlying AST node, if available. if (auto const* expression = dynamic_cast(_sourceNode)) { if (expression->annotation().type) markdown.code(expression->annotation().type->toString(false)); if (auto const* declaration = ASTNode::referencedDeclaration(*expression)) if (declaration->type()) markdown.code(declaration->type()->toString(false)); } else if (auto const* declaration = dynamic_cast(_sourceNode)) { if (declaration->type()) markdown.code(declaration->type()->toString(false)); } else if (auto const* identifierPath = dynamic_cast(_sourceNode)) { Declaration const* decl = identifierPath->annotation().referencedDeclaration; if (decl && decl->type()) markdown.code(decl->type()->toString(false)); if (auto const* node = dynamic_cast(decl)) if (node->documentation()->text()) markdown.text(*node->documentation()->text()); } else if (auto const* expression = dynamic_cast(_sourceNode)) { if (auto const* declaration = ASTNode::referencedDeclaration(*expression)) if (declaration->type()) markdown.code(declaration->type()->toString(false)); } else { markdown.text(fmt::format("Unhandled AST node type in hover: {}\n", typeid(*_sourceNode).name())); log(fmt::format("Unhandled AST node type in hover: {}\n", typeid(*_sourceNode).name())); } // If this AST node contains documentation itself, append it. if (auto const* documented = dynamic_cast(_sourceNode)) { if (documented->documentation()) markdown.text(*documented->documentation()->text()); } return markdown.result.str(); } void LanguageServer::handleTextDocumentHover(MessageID _id, Json::Value const& _args) { auto const sourceNode = requestASTNode(extractDocumentPosition(_args)); string tooltipText = symbolHoverInformation(sourceNode); if (tooltipText.empty()) { m_client->reply(_id, Json::nullValue); return; } Json::Value reply = Json::objectValue; reply["range"] = toRange(sourceNode->location()); reply["contents"]["kind"] = "markdown"; reply["contents"]["value"] = move(tooltipText); m_client->reply(_id, reply); } void LanguageServer::handleTextDocumentHighlight(MessageID _id, Json::Value const& _args) { auto const dpos = extractDocumentPosition(_args); ASTNode const* sourceNode = requestASTNode(dpos); Json::Value jsonReply = Json::arrayValue; for (DocumentHighlight const& highlight: semanticHighlight(sourceNode, dpos.path)) { Json::Value item = Json::objectValue; item["range"] = toRange(highlight.location); if (highlight.kind != DocumentHighlightKind::Unspecified) item["kind"] = int(highlight.kind); jsonReply.append(item); } m_client->reply(_id, jsonReply); } void LanguageServer::handleTextDocumentReferences(MessageID _id, Json::Value const& _args) { auto const dpos = extractDocumentPosition(_args); auto const sourceNode = requestASTNode(dpos); if (!sourceNode) { Json::Value emptyResponse = Json::arrayValue; m_client->reply(_id, emptyResponse); // reply with "No references". return; } string sourceUnitName = clientPathToSourceUnitName(dpos.path); SourceUnit const& sourceUnit = m_compilerStack.ast(sourceUnitName); auto output = vector{}; if (auto const* identifier = dynamic_cast(sourceNode)) { for (auto const* declaration: allAnnotatedDeclarations(identifier)) output += findAllReferences(declaration, declaration->name(), sourceUnit); } else if (auto const* identifierPath = dynamic_cast(sourceNode)) { if (auto decl = identifierPath->annotation().referencedDeclaration) output += findAllReferences(decl, decl->name(), sourceUnit); } else if (auto const* memberAccess = dynamic_cast(sourceNode)) { output += findAllReferences(memberAccess->annotation().referencedDeclaration, memberAccess->memberName(), sourceUnit); } else if (auto const* declaration = dynamic_cast(sourceNode)) { output += findAllReferences(declaration, declaration->name(), sourceUnit); } Json::Value jsonReply = Json::arrayValue; for (SourceLocation const& location: output) jsonReply.append(toJson(location)); log("Sending reply"); m_client->reply(_id, jsonReply); } void LanguageServer::semanticTokensFull(MessageID _id, Json::Value const& _args) { auto uri = _args["textDocument"]["uri"]; if (!compile(uri.as())) return; auto const sourceName = clientPathToSourceUnitName(uri.as()); SourceUnit const& ast = m_compilerStack.ast(sourceName); m_compilerStack.charStream(sourceName); SemanticTokensBuilder semanticTokensBuilder; Json::Value data = semanticTokensBuilder.build(ast, m_compilerStack.charStream(sourceName)); Json::Value reply = Json::objectValue; reply["data"] = data; m_client->reply(_id, reply); } void LanguageServer::terminate() { m_exitRequested = true; }