blob: 7bce042f8900c5fd84424cf8bb77784254d13d44 [file] [log] [blame]
//===--- Protocol.cpp - Language Server Protocol Implementation -----------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains the serialization code for the LSP structs.
//
//===----------------------------------------------------------------------===//
#include "Protocol.h"
#include "llvm/ADT/Hashing.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringSwitch.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/Format.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/JSON.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
using namespace mlir;
using namespace mlir::lsp;
// Helper that doesn't treat `null` and absent fields as failures.
template <typename T>
static bool mapOptOrNull(const llvm::json::Value &params,
llvm::StringLiteral prop, T &out,
llvm::json::Path path) {
const llvm::json::Object *o = params.getAsObject();
assert(o);
// Field is missing or null.
auto *v = o->get(prop);
if (!v || v->getAsNull().hasValue())
return true;
return fromJSON(*v, out, path.field(prop));
}
//===----------------------------------------------------------------------===//
// LSPError
//===----------------------------------------------------------------------===//
char LSPError::ID;
//===----------------------------------------------------------------------===//
// URIForFile
//===----------------------------------------------------------------------===//
static bool isWindowsPath(StringRef path) {
return path.size() > 1 && llvm::isAlpha(path[0]) && path[1] == ':';
}
static bool isNetworkPath(StringRef path) {
return path.size() > 2 && path[0] == path[1] &&
llvm::sys::path::is_separator(path[0]);
}
static bool shouldEscapeInURI(unsigned char c) {
// Unreserved characters.
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9'))
return false;
switch (c) {
case '-':
case '_':
case '.':
case '~':
// '/' is only reserved when parsing.
case '/':
// ':' is only reserved for relative URI paths, which we doesn't produce.
case ':':
return false;
}
return true;
}
/// Encodes a string according to percent-encoding.
/// - Unreserved characters are not escaped.
/// - Reserved characters always escaped with exceptions like '/'.
/// - All other characters are escaped.
static void percentEncode(StringRef content, std::string &out) {
for (unsigned char c : content) {
if (shouldEscapeInURI(c)) {
out.push_back('%');
out.push_back(llvm::hexdigit(c / 16));
out.push_back(llvm::hexdigit(c % 16));
} else {
out.push_back(c);
}
}
}
/// Decodes a string according to percent-encoding.
static std::string percentDecode(StringRef content) {
std::string result;
for (auto i = content.begin(), e = content.end(); i != e; ++i) {
if (*i != '%') {
result += *i;
continue;
}
if (*i == '%' && i + 2 < content.end() && llvm::isHexDigit(*(i + 1)) &&
llvm::isHexDigit(*(i + 2))) {
result.push_back(llvm::hexFromNibbles(*(i + 1), *(i + 2)));
i += 2;
} else {
result.push_back(*i);
}
}
return result;
}
static bool isValidScheme(StringRef scheme) {
if (scheme.empty())
return false;
if (!llvm::isAlpha(scheme[0]))
return false;
return std::all_of(scheme.begin() + 1, scheme.end(), [](char c) {
return llvm::isAlnum(c) || c == '+' || c == '.' || c == '-';
});
}
static llvm::Expected<std::string> uriFromAbsolutePath(StringRef absolutePath) {
std::string body;
StringRef authority;
StringRef root = llvm::sys::path::root_name(absolutePath);
if (isNetworkPath(root)) {
// Windows UNC paths e.g. \\server\share => file://server/share
authority = root.drop_front(2);
absolutePath.consume_front(root);
} else if (isWindowsPath(root)) {
// Windows paths e.g. X:\path => file:///X:/path
body = "/";
}
body += llvm::sys::path::convert_to_slash(absolutePath);
std::string uri = "file:";
if (authority.empty() && body.empty())
return uri;
// If authority if empty, we only print body if it starts with "/"; otherwise,
// the URI is invalid.
if (!authority.empty() || StringRef(body).startswith("/")) {
uri.append("//");
percentEncode(authority, uri);
}
percentEncode(body, uri);
return uri;
}
static llvm::Expected<std::string> getAbsolutePath(StringRef authority,
StringRef body) {
if (!body.startswith("/"))
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"File scheme: expect body to be an absolute path starting "
"with '/': " +
body);
SmallString<128> path;
if (!authority.empty()) {
// Windows UNC paths e.g. file://server/share => \\server\share
("//" + authority).toVector(path);
} else if (isWindowsPath(body.substr(1))) {
// Windows paths e.g. file:///X:/path => X:\path
body.consume_front("/");
}
path.append(body);
llvm::sys::path::native(path);
return std::string(path);
}
static llvm::Expected<std::string> parseFilePathFromURI(StringRef origUri) {
StringRef uri = origUri;
// Decode the scheme of the URI.
size_t pos = uri.find(':');
if (pos == StringRef::npos)
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Scheme must be provided in URI: " +
origUri);
StringRef schemeStr = uri.substr(0, pos);
std::string uriScheme = percentDecode(schemeStr);
if (!isValidScheme(uriScheme))
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"Invalid scheme: " + schemeStr +
" (decoded: " + uriScheme + ")");
uri = uri.substr(pos + 1);
// Decode the authority of the URI.
std::string uriAuthority;
if (uri.consume_front("//")) {
pos = uri.find('/');
uriAuthority = percentDecode(uri.substr(0, pos));
uri = uri.substr(pos);
}
// Decode the body of the URI.
std::string uriBody = percentDecode(uri);
// Compute the absolute path for this uri.
if (uriScheme != "file" && uriScheme != "test") {
return llvm::createStringError(
llvm::inconvertibleErrorCode(),
"mlir-lsp-server only supports 'file' URI scheme for workspace files");
}
return getAbsolutePath(uriAuthority, uriBody);
}
llvm::Expected<URIForFile> URIForFile::fromURI(StringRef uri) {
llvm::Expected<std::string> filePath = parseFilePathFromURI(uri);
if (!filePath)
return filePath.takeError();
return URIForFile(std::move(*filePath), uri.str());
}
llvm::Expected<URIForFile> URIForFile::fromFile(StringRef absoluteFilepath) {
llvm::Expected<std::string> uri = uriFromAbsolutePath(absoluteFilepath);
if (!uri)
return uri.takeError();
return fromURI(*uri);
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value, URIForFile &result,
llvm::json::Path path) {
if (Optional<StringRef> str = value.getAsString()) {
llvm::Expected<URIForFile> expectedURI = URIForFile::fromURI(*str);
if (!expectedURI) {
path.report("unresolvable URI");
consumeError(expectedURI.takeError());
return false;
}
result = std::move(*expectedURI);
return true;
}
return false;
}
llvm::json::Value mlir::lsp::toJSON(const URIForFile &value) {
return value.uri();
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, const URIForFile &value) {
return os << value.uri();
}
//===----------------------------------------------------------------------===//
// ClientCapabilities
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
ClientCapabilities &result, llvm::json::Path path) {
const llvm::json::Object *o = value.getAsObject();
if (!o) {
path.report("expected object");
return false;
}
if (const llvm::json::Object *textDocument = o->getObject("textDocument")) {
if (const llvm::json::Object *documentSymbol =
textDocument->getObject("documentSymbol")) {
if (Optional<bool> hierarchicalSupport =
documentSymbol->getBoolean("hierarchicalDocumentSymbolSupport"))
result.hierarchicalDocumentSymbol = *hierarchicalSupport;
}
}
return true;
}
//===----------------------------------------------------------------------===//
// InitializeParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value, TraceLevel &result,
llvm::json::Path path) {
if (Optional<StringRef> str = value.getAsString()) {
if (*str == "off") {
result = TraceLevel::Off;
return true;
}
if (*str == "messages") {
result = TraceLevel::Messages;
return true;
}
if (*str == "verbose") {
result = TraceLevel::Verbose;
return true;
}
}
return false;
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
InitializeParams &result, llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
if (!o)
return false;
// We deliberately don't fail if we can't parse individual fields.
o.map("capabilities", result.capabilities);
o.map("trace", result.trace);
return true;
}
//===----------------------------------------------------------------------===//
// TextDocumentItem
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
TextDocumentItem &result, llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("uri", result.uri) &&
o.map("languageId", result.languageId) && o.map("text", result.text) &&
o.map("version", result.version);
}
//===----------------------------------------------------------------------===//
// TextDocumentIdentifier
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const TextDocumentIdentifier &value) {
return llvm::json::Object{{"uri", value.uri}};
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
TextDocumentIdentifier &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("uri", result.uri);
}
//===----------------------------------------------------------------------===//
// VersionedTextDocumentIdentifier
//===----------------------------------------------------------------------===//
llvm::json::Value
mlir::lsp::toJSON(const VersionedTextDocumentIdentifier &value) {
return llvm::json::Object{
{"uri", value.uri},
{"version", value.version},
};
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
VersionedTextDocumentIdentifier &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("uri", result.uri) && o.map("version", result.version);
}
//===----------------------------------------------------------------------===//
// Position
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value, Position &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("line", result.line) &&
o.map("character", result.character);
}
llvm::json::Value mlir::lsp::toJSON(const Position &value) {
return llvm::json::Object{
{"line", value.line},
{"character", value.character},
};
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, const Position &value) {
return os << value.line << ':' << value.character;
}
//===----------------------------------------------------------------------===//
// Range
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value, Range &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("start", result.start) && o.map("end", result.end);
}
llvm::json::Value mlir::lsp::toJSON(const Range &value) {
return llvm::json::Object{
{"start", value.start},
{"end", value.end},
};
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, const Range &value) {
return os << value.start << '-' << value.end;
}
//===----------------------------------------------------------------------===//
// Location
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const Location &value) {
return llvm::json::Object{
{"uri", value.uri},
{"range", value.range},
};
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, const Location &value) {
return os << value.range << '@' << value.uri;
}
//===----------------------------------------------------------------------===//
// TextDocumentPositionParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
TextDocumentPositionParams &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("textDocument", result.textDocument) &&
o.map("position", result.position);
}
//===----------------------------------------------------------------------===//
// ReferenceParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
ReferenceContext &result, llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.mapOptional("includeDeclaration", result.includeDeclaration);
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
ReferenceParams &result, llvm::json::Path path) {
TextDocumentPositionParams &base = result;
llvm::json::ObjectMapper o(value, path);
return fromJSON(value, base, path) && o &&
o.mapOptional("context", result.context);
}
//===----------------------------------------------------------------------===//
// DidOpenTextDocumentParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
DidOpenTextDocumentParams &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("textDocument", result.textDocument);
}
//===----------------------------------------------------------------------===//
// DidCloseTextDocumentParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
DidCloseTextDocumentParams &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("textDocument", result.textDocument);
}
//===----------------------------------------------------------------------===//
// DidChangeTextDocumentParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
TextDocumentContentChangeEvent &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("range", result.range) &&
o.map("rangeLength", result.rangeLength) && o.map("text", result.text);
}
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
DidChangeTextDocumentParams &result,
llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("textDocument", result.textDocument) &&
o.map("contentChanges", result.contentChanges);
}
//===----------------------------------------------------------------------===//
// MarkupContent
//===----------------------------------------------------------------------===//
static llvm::StringRef toTextKind(MarkupKind kind) {
switch (kind) {
case MarkupKind::PlainText:
return "plaintext";
case MarkupKind::Markdown:
return "markdown";
}
llvm_unreachable("Invalid MarkupKind");
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, MarkupKind kind) {
return os << toTextKind(kind);
}
llvm::json::Value mlir::lsp::toJSON(const MarkupContent &mc) {
if (mc.value.empty())
return nullptr;
return llvm::json::Object{
{"kind", toTextKind(mc.kind)},
{"value", mc.value},
};
}
//===----------------------------------------------------------------------===//
// Hover
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const Hover &hover) {
llvm::json::Object result{{"contents", toJSON(hover.contents)}};
if (hover.range.hasValue())
result["range"] = toJSON(*hover.range);
return std::move(result);
}
//===----------------------------------------------------------------------===//
// DocumentSymbol
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const DocumentSymbol &symbol) {
llvm::json::Object result{{"name", symbol.name},
{"kind", static_cast<int>(symbol.kind)},
{"range", symbol.range},
{"selectionRange", symbol.selectionRange}};
if (!symbol.detail.empty())
result["detail"] = symbol.detail;
if (!symbol.children.empty())
result["children"] = symbol.children;
return std::move(result);
}
//===----------------------------------------------------------------------===//
// DocumentSymbolParams
//===----------------------------------------------------------------------===//
bool mlir::lsp::fromJSON(const llvm::json::Value &value,
DocumentSymbolParams &result, llvm::json::Path path) {
llvm::json::ObjectMapper o(value, path);
return o && o.map("textDocument", result.textDocument);
}
//===----------------------------------------------------------------------===//
// DiagnosticRelatedInformation
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const DiagnosticRelatedInformation &info) {
return llvm::json::Object{
{"location", info.location},
{"message", info.message},
};
}
//===----------------------------------------------------------------------===//
// Diagnostic
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const Diagnostic &diag) {
llvm::json::Object result{
{"range", diag.range},
{"severity", (int)diag.severity},
{"message", diag.message},
};
if (diag.category)
result["category"] = *diag.category;
if (!diag.source.empty())
result["source"] = diag.source;
if (diag.relatedInformation)
result["relatedInformation"] = *diag.relatedInformation;
return std::move(result);
}
//===----------------------------------------------------------------------===//
// PublishDiagnosticsParams
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const PublishDiagnosticsParams &params) {
return llvm::json::Object{
{"uri", params.uri},
{"diagnostics", params.diagnostics},
{"version", params.version},
};
}