[mlir-lsp-server] Add support for sending diagnostics to the client

This allows for diagnostics emitted during parsing/verification to be surfaced to the user by the language client, as opposed to just being emitted to the logs like they are now.

Differential Revision: https://reviews.llvm.org/D102293
This commit is contained in:
River Riddle 2021-05-12 13:01:59 -07:00
parent 30b7dfafdb
commit b3911cdfc8
10 changed files with 389 additions and 119 deletions

View File

@ -82,6 +82,7 @@ public:
AsmParserState();
~AsmParserState();
AsmParserState &operator=(AsmParserState &&other);
//===--------------------------------------------------------------------===//
// Access State

View File

@ -52,6 +52,10 @@ struct AsmParserState::Impl {
AsmParserState::AsmParserState() : impl(std::make_unique<Impl>()) {}
AsmParserState::~AsmParserState() {}
AsmParserState &AsmParserState::operator=(AsmParserState &&other) {
impl = std::move(other.impl);
return *this;
}
//===----------------------------------------------------------------------===//
// Access State

View File

@ -59,6 +59,10 @@ struct LSPServer::Impl {
MLIRServer &server;
JSONTransport &transport;
/// An outgoing notification used to send diagnostics to the client when they
/// are ready to be processed.
OutgoingNotification<PublishDiagnosticsParams> publishDiagnostics;
/// Used to indicate that the 'shutdown' request was received from the
/// Language Server client.
bool shutdownRequestReceived = false;
@ -99,11 +103,21 @@ void LSPServer::Impl::onShutdown(const NoParams &,
void LSPServer::Impl::onDocumentDidOpen(
const DidOpenTextDocumentParams &params) {
server.addOrUpdateDocument(params.textDocument.uri, params.textDocument.text);
PublishDiagnosticsParams diagParams(params.textDocument.uri);
server.addOrUpdateDocument(params.textDocument.uri, params.textDocument.text,
diagParams.diagnostics);
// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
}
void LSPServer::Impl::onDocumentDidClose(
const DidCloseTextDocumentParams &params) {
server.removeDocument(params.textDocument.uri);
// Empty out the diagnostics shown for this document. This will clear out
// anything currently displayed by the client for this document (e.g. in the
// "Problems" pane of VSCode).
publishDiagnostics(PublishDiagnosticsParams(params.textDocument.uri));
}
void LSPServer::Impl::onDocumentDidChange(
const DidChangeTextDocumentParams &params) {
@ -111,8 +125,13 @@ void LSPServer::Impl::onDocumentDidChange(
// to avoid this.
if (params.contentChanges.size() != 1)
return;
PublishDiagnosticsParams diagParams(params.textDocument.uri);
server.addOrUpdateDocument(params.textDocument.uri,
params.contentChanges.front().text);
params.contentChanges.front().text,
diagParams.diagnostics);
// Publish any recorded diagnostics.
publishDiagnostics(diagParams);
}
//===----------------------------------------------------------------------===//
@ -173,6 +192,11 @@ LogicalResult LSPServer::run() {
// Hover
messageHandler.method("textDocument/hover", impl.get(), &Impl::onHover);
// Diagnostics
impl->publishDiagnostics =
messageHandler.outgoingNotification<PublishDiagnosticsParams>(
"textDocument/publishDiagnostics");
// Run the main loop of the transport.
LogicalResult result = success();
if (llvm::Error error = impl->transport.run(messageHandler)) {

View File

@ -60,7 +60,28 @@ static Optional<lsp::Location> getLocationFromLoc(FileLineColLoc loc) {
lsp::Position position;
position.line = loc.getLine() - 1;
position.character = loc.getColumn();
return lsp::Location{*sourceURI, lsp::Range{position, position}};
return lsp::Location{*sourceURI, lsp::Range(position)};
}
/// Returns a language server location from the given MLIR location, or None if
/// one couldn't be created. `uri` is an optional additional filter that, when
/// present, is used to filter sub locations that do not share the same uri.
static Optional<lsp::Location>
getLocationFromLoc(Location loc, const lsp::URIForFile *uri = nullptr) {
Optional<lsp::Location> location;
loc->walk([&](Location nestedLoc) {
FileLineColLoc fileLoc = nestedLoc.dyn_cast<FileLineColLoc>();
if (!fileLoc)
return WalkResult::advance();
Optional<lsp::Location> sourceLoc = getLocationFromLoc(fileLoc);
if (sourceLoc && (!uri || sourceLoc->uri == *uri)) {
location = *sourceLoc;
return WalkResult::interrupt();
}
return WalkResult::advance();
});
return location;
}
/// Collect all of the locations from the given MLIR location that are not
@ -173,6 +194,56 @@ static void printDefBlockName(raw_ostream &os,
printDefBlockName(os, def.block, def.definition.loc);
}
/// Convert the given MLIR diagnostic to the LSP form.
static lsp::Diagnostic getLspDiagnoticFromDiag(Diagnostic &diag,
const lsp::URIForFile &uri) {
lsp::Diagnostic lspDiag;
lspDiag.source = "mlir";
// Note: Right now all of the diagnostics are treated as parser issues, but
// some are parser and some are verifier.
lspDiag.category = "Parse Error";
// Try to grab a file location for this diagnostic.
// TODO: For simplicity, we just grab the first one. It may be likely that we
// will need a more interesting heuristic here.'
Optional<lsp::Location> lspLocation =
getLocationFromLoc(diag.getLocation(), &uri);
if (lspLocation)
lspDiag.range = lspLocation->range;
// Convert the severity for the diagnostic.
switch (diag.getSeverity()) {
case DiagnosticSeverity::Note:
llvm_unreachable("expected notes to be handled separately");
case DiagnosticSeverity::Warning:
lspDiag.severity = lsp::DiagnosticSeverity::Warning;
break;
case DiagnosticSeverity::Error:
lspDiag.severity = lsp::DiagnosticSeverity::Error;
break;
case DiagnosticSeverity::Remark:
lspDiag.severity = lsp::DiagnosticSeverity::Information;
break;
}
lspDiag.message = diag.str();
// Attach any notes to the main diagnostic as related information.
std::vector<lsp::DiagnosticRelatedInformation> relatedDiags;
for (Diagnostic &note : diag.getNotes()) {
lsp::Location noteLoc;
if (Optional<lsp::Location> loc = getLocationFromLoc(note.getLocation()))
noteLoc = *loc;
else
noteLoc.uri = uri;
relatedDiags.emplace_back(noteLoc, note.str());
}
if (!relatedDiags.empty())
lspDiag.relatedInformation = std::move(relatedDiags);
return lspDiag;
}
//===----------------------------------------------------------------------===//
// MLIRDocument
//===----------------------------------------------------------------------===//
@ -182,7 +253,8 @@ namespace {
/// document.
struct MLIRDocument {
MLIRDocument(const lsp::URIForFile &uri, StringRef contents,
DialectRegistry &registry);
DialectRegistry &registry,
std::vector<lsp::Diagnostic> &diagnostics);
//===--------------------------------------------------------------------===//
// Definitions and References
@ -227,15 +299,12 @@ struct MLIRDocument {
} // namespace
MLIRDocument::MLIRDocument(const lsp::URIForFile &uri, StringRef contents,
DialectRegistry &registry)
DialectRegistry &registry,
std::vector<lsp::Diagnostic> &diagnostics)
: context(registry) {
context.allowUnregisteredDialects();
ScopedDiagnosticHandler handler(&context, [&](Diagnostic &diag) {
// TODO: What should we do with these diagnostics?
// * Cache and show to the user?
// * Ignore?
lsp::Logger::error("Error when parsing MLIR document `{0}`: `{1}`",
uri.file(), diag.str());
diagnostics.push_back(getLspDiagnoticFromDiag(diag, uri));
});
// Try to parsed the given IR string.
@ -246,9 +315,13 @@ MLIRDocument::MLIRDocument(const lsp::URIForFile &uri, StringRef contents,
}
sourceMgr.AddNewSourceBuffer(std::move(memBuffer), llvm::SMLoc());
if (failed(
parseSourceFile(sourceMgr, &parsedIR, &context, nullptr, &asmState)))
if (failed(parseSourceFile(sourceMgr, &parsedIR, &context, nullptr,
&asmState))) {
// If parsing failed, clear out any of the current state.
parsedIR.clear();
asmState = AsmParserState();
return;
}
}
//===----------------------------------------------------------------------===//
@ -495,10 +568,11 @@ lsp::MLIRServer::MLIRServer(DialectRegistry &registry)
: impl(std::make_unique<Impl>(registry)) {}
lsp::MLIRServer::~MLIRServer() {}
void lsp::MLIRServer::addOrUpdateDocument(const URIForFile &uri,
StringRef contents) {
impl->documents[uri.file()] =
std::make_unique<MLIRDocument>(uri, contents, impl->registry);
void lsp::MLIRServer::addOrUpdateDocument(
const URIForFile &uri, StringRef contents,
std::vector<Diagnostic> &diagnostics) {
impl->documents[uri.file()] = std::make_unique<MLIRDocument>(
uri, contents, impl->registry, diagnostics);
}
void lsp::MLIRServer::removeDocument(const URIForFile &uri) {

View File

@ -16,6 +16,7 @@ namespace mlir {
class DialectRegistry;
namespace lsp {
struct Diagnostic;
struct Hover;
struct Location;
struct Position;
@ -30,8 +31,10 @@ public:
MLIRServer(DialectRegistry &registry);
~MLIRServer();
/// Add or update the document at the given URI.
void addOrUpdateDocument(const URIForFile &uri, StringRef contents);
/// Add or update the document at the given URI. Any diagnostics emitted for
/// this document should be added to `diagnostics`
void addOrUpdateDocument(const URIForFile &uri, StringRef contents,
std::vector<Diagnostic> &diagnostics);
/// Remove the document with the given uri.
void removeDocument(const URIForFile &uri);

View File

@ -473,3 +473,44 @@ llvm::json::Value mlir::lsp::toJSON(const Hover &hover) {
result["range"] = toJSON(*hover.range);
return std::move(result);
}
//===----------------------------------------------------------------------===//
// 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},
};
}

View File

@ -228,6 +228,10 @@ raw_ostream &operator<<(raw_ostream &os, const Position &value);
//===----------------------------------------------------------------------===//
struct Range {
Range() = default;
Range(Position start, Position end) : start(start), end(end) {}
Range(Position loc) : Range(loc, loc) {}
/// The range's start position.
Position start;
@ -393,6 +397,79 @@ struct Hover {
};
llvm::json::Value toJSON(const Hover &hover);
//===----------------------------------------------------------------------===//
// DiagnosticRelatedInformation
//===----------------------------------------------------------------------===//
/// Represents a related message and source code location for a diagnostic.
/// This should be used to point to code locations that cause or related to a
/// diagnostics, e.g. when duplicating a symbol in a scope.
struct DiagnosticRelatedInformation {
DiagnosticRelatedInformation(Location location, std::string message)
: location(location), message(std::move(message)) {}
/// The location of this related diagnostic information.
Location location;
/// The message of this related diagnostic information.
std::string message;
};
llvm::json::Value toJSON(const DiagnosticRelatedInformation &info);
//===----------------------------------------------------------------------===//
// Diagnostic
//===----------------------------------------------------------------------===//
enum class DiagnosticSeverity {
/// It is up to the client to interpret diagnostics as error, warning, info or
/// hint.
Undetermined = 0,
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
};
struct Diagnostic {
/// The source range where the message applies.
Range range;
/// The diagnostic's severity. Can be omitted. If omitted it is up to the
/// client to interpret diagnostics as error, warning, info or hint.
DiagnosticSeverity severity = DiagnosticSeverity::Undetermined;
/// A human-readable string describing the source of this diagnostic, e.g.
/// 'typescript' or 'super lint'.
std::string source;
/// The diagnostic's message.
std::string message;
/// An array of related diagnostic information, e.g. when symbol-names within
/// a scope collide all definitions can be marked via this property.
Optional<std::vector<DiagnosticRelatedInformation>> relatedInformation;
/// The diagnostic's category. Can be omitted.
/// An LSP extension that's used to send the name of the category over to the
/// client. The category typically describes the compilation stage during
/// which the issue was produced, e.g. "Semantic Issue" or "Parse Issue".
Optional<std::string> category;
};
llvm::json::Value toJSON(const Diagnostic &diag);
//===----------------------------------------------------------------------===//
// PublishDiagnosticsParams
//===----------------------------------------------------------------------===//
struct PublishDiagnosticsParams {
PublishDiagnosticsParams(URIForFile uri) : uri(uri) {}
/// The URI for which diagnostic information is reported.
URIForFile uri;
/// The list of reported diagnostics.
std::vector<Diagnostic> diagnostics;
};
llvm::json::Value toJSON(const PublishDiagnosticsParams &params);
} // namespace lsp
} // namespace mlir

View File

@ -21,6 +21,30 @@ using namespace mlir::lsp;
// Reply
//===----------------------------------------------------------------------===//
namespace {
/// Function object to reply to an LSP call.
/// Each instance must be called exactly once, otherwise:
/// - if there was no reply, an error reply is sent
/// - if there were multiple replies, only the first is sent
class Reply {
public:
Reply(const llvm::json::Value &id, StringRef method,
JSONTransport &transport);
Reply(Reply &&other);
Reply &operator=(Reply &&) = delete;
Reply(const Reply &) = delete;
Reply &operator=(const Reply &) = delete;
void operator()(llvm::Expected<llvm::json::Value> reply);
private:
StringRef method;
std::atomic<bool> replied = {false};
llvm::json::Value id;
JSONTransport *transport;
};
} // namespace
Reply::Reply(const llvm::json::Value &id, llvm::StringRef method,
JSONTransport &transport)
: id(id), transport(&transport) {}

View File

@ -28,107 +28,7 @@
namespace mlir {
namespace lsp {
class JSONTransport;
//===----------------------------------------------------------------------===//
// Reply
//===----------------------------------------------------------------------===//
/// Function object to reply to an LSP call.
/// Each instance must be called exactly once, otherwise:
/// - if there was no reply, an error reply is sent
/// - if there were multiple replies, only the first is sent
class Reply {
public:
Reply(const llvm::json::Value &id, StringRef method,
JSONTransport &transport);
Reply(Reply &&other);
Reply &operator=(Reply &&) = delete;
Reply(const Reply &) = delete;
Reply &operator=(const Reply &) = delete;
void operator()(llvm::Expected<llvm::json::Value> reply);
private:
StringRef method;
std::atomic<bool> replied = {false};
llvm::json::Value id;
JSONTransport *transport;
};
//===----------------------------------------------------------------------===//
// MessageHandler
//===----------------------------------------------------------------------===//
/// A Callback<T> is a void function that accepts Expected<T>. This is
/// accepted by functions that logically return T.
template <typename T>
using Callback = llvm::unique_function<void(llvm::Expected<T>)>;
/// A handler used to process the incoming transport messages.
class MessageHandler {
public:
MessageHandler(JSONTransport &transport) : transport(transport) {}
bool onNotify(StringRef method, llvm::json::Value value);
bool onCall(StringRef method, llvm::json::Value params, llvm::json::Value id);
bool onReply(llvm::json::Value id, llvm::Expected<llvm::json::Value> result);
template <typename T>
static llvm::Expected<T> parse(const llvm::json::Value &raw,
StringRef payloadName, StringRef payloadKind) {
T result;
llvm::json::Path::Root root;
if (fromJSON(raw, result, root))
return std::move(result);
// Dump the relevant parts of the broken message.
std::string context;
llvm::raw_string_ostream os(context);
root.printErrorContext(raw, os);
// Report the error (e.g. to the client).
return llvm::make_error<LSPError>(
llvm::formatv("failed to decode {0} {1}: {2}", payloadName, payloadKind,
fmt_consume(root.getError())),
ErrorCode::InvalidParams);
}
template <typename Param, typename Result, typename ThisT>
void method(llvm::StringLiteral method, ThisT *thisPtr,
void (ThisT::*handler)(const Param &, Callback<Result>)) {
methodHandlers[method] = [method, handler,
thisPtr](llvm::json::Value rawParams,
Callback<llvm::json::Value> reply) {
llvm::Expected<Param> param = parse<Param>(rawParams, method, "request");
if (!param)
return reply(param.takeError());
(thisPtr->*handler)(*param, std::move(reply));
};
}
template <typename Param, typename ThisT>
void notification(llvm::StringLiteral method, ThisT *thisPtr,
void (ThisT::*handler)(const Param &)) {
notificationHandlers[method] = [method, handler,
thisPtr](llvm::json::Value rawParams) {
llvm::Expected<Param> param = parse<Param>(rawParams, method, "request");
if (!param)
return llvm::consumeError(param.takeError());
(thisPtr->*handler)(*param);
};
}
private:
template <typename HandlerT>
using HandlerMap = llvm::StringMap<llvm::unique_function<HandlerT>>;
HandlerMap<void(llvm::json::Value)> notificationHandlers;
HandlerMap<void(llvm::json::Value, Callback<llvm::json::Value>)>
methodHandlers;
JSONTransport &transport;
};
class MessageHandler;
//===----------------------------------------------------------------------===//
// JSONTransport
@ -185,6 +85,93 @@ private:
bool prettyOutput;
};
//===----------------------------------------------------------------------===//
// MessageHandler
//===----------------------------------------------------------------------===//
/// A Callback<T> is a void function that accepts Expected<T>. This is
/// accepted by functions that logically return T.
template <typename T>
using Callback = llvm::unique_function<void(llvm::Expected<T>)>;
/// An OutgoingNotification<T> is a function used for outgoing notifications
/// send to the client.
template <typename T>
using OutgoingNotification = llvm::unique_function<void(const T &)>;
/// A handler used to process the incoming transport messages.
class MessageHandler {
public:
MessageHandler(JSONTransport &transport) : transport(transport) {}
bool onNotify(StringRef method, llvm::json::Value value);
bool onCall(StringRef method, llvm::json::Value params, llvm::json::Value id);
bool onReply(llvm::json::Value id, llvm::Expected<llvm::json::Value> result);
template <typename T>
static llvm::Expected<T> parse(const llvm::json::Value &raw,
StringRef payloadName, StringRef payloadKind) {
T result;
llvm::json::Path::Root root;
if (fromJSON(raw, result, root))
return std::move(result);
// Dump the relevant parts of the broken message.
std::string context;
llvm::raw_string_ostream os(context);
root.printErrorContext(raw, os);
// Report the error (e.g. to the client).
return llvm::make_error<LSPError>(
llvm::formatv("failed to decode {0} {1}: {2}", payloadName, payloadKind,
fmt_consume(root.getError())),
ErrorCode::InvalidParams);
}
template <typename Param, typename Result, typename ThisT>
void method(llvm::StringLiteral method, ThisT *thisPtr,
void (ThisT::*handler)(const Param &, Callback<Result>)) {
methodHandlers[method] = [method, handler,
thisPtr](llvm::json::Value rawParams,
Callback<llvm::json::Value> reply) {
llvm::Expected<Param> param = parse<Param>(rawParams, method, "request");
if (!param)
return reply(param.takeError());
(thisPtr->*handler)(*param, std::move(reply));
};
}
template <typename Param, typename ThisT>
void notification(llvm::StringLiteral method, ThisT *thisPtr,
void (ThisT::*handler)(const Param &)) {
notificationHandlers[method] = [method, handler,
thisPtr](llvm::json::Value rawParams) {
llvm::Expected<Param> param = parse<Param>(rawParams, method, "request");
if (!param)
return llvm::consumeError(param.takeError());
(thisPtr->*handler)(*param);
};
}
/// Create an OutgoingNotification object used for the given method.
template <typename T>
OutgoingNotification<T> outgoingNotification(llvm::StringLiteral method) {
return [&, method](const T &params) {
transport.notify(method, llvm::json::Value(params));
};
}
private:
template <typename HandlerT>
using HandlerMap = llvm::StringMap<llvm::unique_function<HandlerT>>;
HandlerMap<void(llvm::json::Value)> notificationHandlers;
HandlerMap<void(llvm::json::Value, Callback<llvm::json::Value>)>
methodHandlers;
JSONTransport &transport;
};
} // namespace lsp
} // namespace mlir

View File

@ -0,0 +1,35 @@
// RUN: mlir-lsp-server -lit-test < %s | FileCheck -strict-whitespace %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"mlir","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///foo.mlir",
"languageId":"mlir",
"version":1,
"text":"func ()"
}}}
// CHECK: "method": "textDocument/publishDiagnostics",
// CHECK-NEXT: "params": {
// CHECK-NEXT: "diagnostics": [
// CHECK-NEXT: {
// CHECK-NEXT: "category": "Parse Error",
// CHECK-NEXT: "message": "custom op 'func' expected valid '@'-identifier for symbol name",
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 6,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 6,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: }
// CHECK-NEXT: },
// CHECK-NEXT: "severity": 1,
// CHECK-NEXT: "source": "mlir"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "uri": "test:///foo.mlir"
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}