1// Copyright 2019 the V8 project authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <algorithm>
6#include "src/torque/ls/message-handler.h"
7
8#include "src/torque/ls/globals.h"
9#include "src/torque/ls/json-parser.h"
10#include "src/torque/ls/message-pipe.h"
11#include "src/torque/ls/message.h"
12#include "src/torque/server-data.h"
13#include "src/torque/source-positions.h"
14#include "src/torque/torque-compiler.h"
15
16namespace v8 {
17namespace internal {
18namespace torque {
19
20DEFINE_CONTEXTUAL_VARIABLE(Logger)
21DEFINE_CONTEXTUAL_VARIABLE(TorqueFileList)
22DEFINE_CONTEXTUAL_VARIABLE(DiagnosticsFiles)
23
24namespace ls {
25
26static const char kContentLength[] = "Content-Length: ";
27static const size_t kContentLengthSize = sizeof(kContentLength) - 1;
28
29#ifdef V8_OS_WIN
30// On Windows, in text mode, \n is translated to \r\n.
31constexpr const char* kProtocolLineEnding = "\n\n";
32#else
33constexpr const char* kProtocolLineEnding = "\r\n\r\n";
34#endif
35
36JsonValue ReadMessage() {
37  std::string line;
38  std::getline(std::cin, line);
39
40  if (line.rfind(kContentLength) != 0) {
41    // Invalid message, we just crash.
42    Logger::Log("[fatal] Did not find Content-Length ...\n");
43    v8::base::OS::Abort();
44  }
45
46  const int content_length = std::atoi(line.substr(kContentLengthSize).c_str());
47  std::getline(std::cin, line);
48  std::string content(content_length, ' ');
49  std::cin.read(&content[0], content_length);
50
51  Logger::Log("[incoming] ", content, "\n\n");
52
53  return ParseJson(content).value;
54}
55
56void WriteMessage(JsonValue message) {
57  std::string content = SerializeToString(message);
58
59  Logger::Log("[outgoing] ", content, "\n\n");
60
61  std::cout << kContentLength << content.size() << kProtocolLineEnding;
62  std::cout << content << std::flush;
63}
64
65namespace {
66
67void ResetCompilationErrorDiagnostics(MessageWriter writer) {
68  for (const SourceId& source : DiagnosticsFiles::Get()) {
69    PublishDiagnosticsNotification notification;
70    notification.set_method("textDocument/publishDiagnostics");
71
72    std::string error_file = SourceFileMap::AbsolutePath(source);
73    notification.params().set_uri(error_file);
74    // Trigger empty array creation.
75    USE(notification.params().diagnostics_size());
76
77    writer(std::move(notification.GetJsonValue()));
78  }
79  DiagnosticsFiles::Get() = {};
80}
81
82// Each notification must contain all diagnostics for a specific file,
83// because sending multiple notifications per file resets previously sent
84// diagnostics. Thus, two steps are needed:
85//   1) collect all notifications in this class.
86//   2) send one notification per entry (per file).
87class DiagnosticCollector {
88 public:
89  void AddTorqueMessage(const TorqueMessage& message) {
90    if (!ShouldAddMessageOfKind(message.kind)) return;
91
92    SourceId id =
93        message.position ? message.position->source : SourceId::Invalid();
94    auto& notification = GetOrCreateNotificationForSource(id);
95
96    Diagnostic diagnostic = notification.params().add_diagnostics();
97    diagnostic.set_severity(ServerityFor(message.kind));
98    diagnostic.set_message(message.message);
99    diagnostic.set_source("Torque Compiler");
100
101    if (message.position) {
102      PopulateRangeFromSourcePosition(diagnostic.range(), *message.position);
103    }
104  }
105
106  std::map<SourceId, PublishDiagnosticsNotification>& notifications() {
107    return notifications_;
108  }
109
110 private:
111  PublishDiagnosticsNotification& GetOrCreateNotificationForSource(
112      SourceId id) {
113    auto iter = notifications_.find(id);
114    if (iter != notifications_.end()) return iter->second;
115
116    PublishDiagnosticsNotification& notification = notifications_[id];
117    notification.set_method("textDocument/publishDiagnostics");
118
119    std::string file =
120        id.IsValid() ? SourceFileMap::AbsolutePath(id) : "<unknown>";
121    notification.params().set_uri(file);
122    return notification;
123  }
124
125  bool ShouldAddMessageOfKind(TorqueMessage::Kind kind) {
126    // An error can easily cause a lot of false positive lint messages, due to
127    // unused variables, macros, etc. Thus we suppress subsequent lint messages
128    // when there are errors.
129    switch (kind) {
130      case TorqueMessage::Kind::kError:
131        suppress_lint_messages_ = true;
132        return true;
133      case TorqueMessage::Kind::kLint:
134        if (suppress_lint_messages_) return false;
135        return true;
136    }
137  }
138
139  void PopulateRangeFromSourcePosition(Range range,
140                                       const SourcePosition& position) {
141    range.start().set_line(position.start.line);
142    range.start().set_character(position.start.column);
143    range.end().set_line(position.end.line);
144    range.end().set_character(position.end.column);
145  }
146
147  Diagnostic::DiagnosticSeverity ServerityFor(TorqueMessage::Kind kind) {
148    switch (kind) {
149      case TorqueMessage::Kind::kError:
150        return Diagnostic::kError;
151      case TorqueMessage::Kind::kLint:
152        return Diagnostic::kWarning;
153    }
154  }
155
156  std::map<SourceId, PublishDiagnosticsNotification> notifications_;
157  bool suppress_lint_messages_ = false;
158};
159
160void SendCompilationDiagnostics(const TorqueCompilerResult& result,
161                                MessageWriter writer) {
162  DiagnosticCollector collector;
163
164  // TODO(szuend): Split up messages by SourceId and sort them by line number.
165  for (const TorqueMessage& message : result.messages) {
166    collector.AddTorqueMessage(message);
167  }
168
169  for (auto& pair : collector.notifications()) {
170    PublishDiagnosticsNotification& notification = pair.second;
171    writer(std::move(notification.GetJsonValue()));
172
173    // Record all source files for which notifications are sent, so they
174    // can be reset before the next compiler run.
175    const SourceId& source = pair.first;
176    if (source.IsValid()) DiagnosticsFiles::Get().push_back(source);
177  }
178}
179
180}  // namespace
181
182void CompilationFinished(TorqueCompilerResult result, MessageWriter writer) {
183  LanguageServerData::Get() = std::move(result.language_server_data);
184  SourceFileMap::Get() = *result.source_file_map;
185
186  SendCompilationDiagnostics(result, writer);
187}
188
189namespace {
190
191void RecompileTorque(MessageWriter writer) {
192  Logger::Log("[info] Start compilation run ...\n");
193
194  TorqueCompilerOptions options;
195  options.output_directory = "";
196  options.collect_language_server_data = true;
197  options.force_assert_statements = true;
198
199  TorqueCompilerResult result = CompileTorque(TorqueFileList::Get(), options);
200
201  Logger::Log("[info] Finished compilation run ...\n");
202
203  CompilationFinished(std::move(result), writer);
204}
205
206void RecompileTorqueWithDiagnostics(MessageWriter writer) {
207  ResetCompilationErrorDiagnostics(writer);
208  RecompileTorque(writer);
209}
210
211void HandleInitializeRequest(InitializeRequest request, MessageWriter writer) {
212  InitializeResponse response;
213  response.set_id(request.id());
214  response.result().capabilities().textDocumentSync();
215  response.result().capabilities().set_definitionProvider(true);
216  response.result().capabilities().set_documentSymbolProvider(true);
217
218  // TODO(szuend): Register for document synchronisation here,
219  //               so we work with the content that the client
220  //               provides, not directly read from files.
221  // TODO(szuend): Check that the client actually supports dynamic
222  //               "workspace/didChangeWatchedFiles" capability.
223  // TODO(szuend): Check if client supports "LocationLink". This will
224  //               influence the result of "goto definition".
225  writer(std::move(response.GetJsonValue()));
226}
227
228void HandleInitializedNotification(MessageWriter writer) {
229  RegistrationRequest request;
230  // TODO(szuend): The language server needs a "global" request id counter.
231  request.set_id(2000);
232  request.set_method("client/registerCapability");
233
234  Registration reg = request.params().add_registrations();
235  auto options =
236      reg.registerOptions<DidChangeWatchedFilesRegistrationOptions>();
237  FileSystemWatcher watcher = options.add_watchers();
238  watcher.set_globPattern("**/*.tq");
239  watcher.set_kind(FileSystemWatcher::WatchKind::kAll);
240
241  reg.set_id("did-change-id");
242  reg.set_method("workspace/didChangeWatchedFiles");
243
244  writer(std::move(request.GetJsonValue()));
245}
246
247void HandleTorqueFileListNotification(TorqueFileListNotification notification,
248                                      MessageWriter writer) {
249  CHECK_EQ(notification.params().object()["files"].tag, JsonValue::ARRAY);
250
251  std::vector<std::string>& files = TorqueFileList::Get();
252  Logger::Log("[info] Initial file list:\n");
253  for (const auto& file_json :
254       notification.params().object()["files"].ToArray()) {
255    CHECK(file_json.IsString());
256
257    // We only consider file URIs (there shouldn't be anything else).
258    // Internally we store the URI instead of the path, eliminating the need
259    // to encode it again.
260    files.push_back(file_json.ToString());
261    Logger::Log("    ", file_json.ToString(), "\n");
262  }
263  RecompileTorqueWithDiagnostics(writer);
264}
265
266void HandleGotoDefinitionRequest(GotoDefinitionRequest request,
267                                 MessageWriter writer) {
268  GotoDefinitionResponse response;
269  response.set_id(request.id());
270
271  SourceId id =
272      SourceFileMap::GetSourceId(request.params().textDocument().uri());
273
274  // Unknown source files cause an empty response which corresponds with
275  // the definition not beeing found.
276  if (!id.IsValid()) {
277    response.SetNull("result");
278    writer(std::move(response.GetJsonValue()));
279    return;
280  }
281
282  auto pos =
283      LineAndColumn::WithUnknownOffset(request.params().position().line(),
284                                       request.params().position().character());
285
286  if (auto maybe_definition = LanguageServerData::FindDefinition(id, pos)) {
287    SourcePosition definition = *maybe_definition;
288    response.result().SetTo(definition);
289  } else {
290    response.SetNull("result");
291  }
292
293  writer(std::move(response.GetJsonValue()));
294}
295
296void HandleChangeWatchedFilesNotification(
297    DidChangeWatchedFilesNotification notification, MessageWriter writer) {
298  // TODO(szuend): Implement updates to the TorqueFile list when create/delete
299  //               notifications are received. Currently we simply re-compile.
300  RecompileTorqueWithDiagnostics(writer);
301}
302
303void HandleDocumentSymbolRequest(DocumentSymbolRequest request,
304                                 MessageWriter writer) {
305  DocumentSymbolResponse response;
306  response.set_id(request.id());
307
308  SourceId id =
309      SourceFileMap::GetSourceId(request.params().textDocument().uri());
310
311  for (const auto& symbol : LanguageServerData::SymbolsForSourceId(id)) {
312    DCHECK(symbol->IsUserDefined());
313    if (symbol->IsMacro()) {
314      Macro* macro = Macro::cast(symbol);
315      SymbolInformation info = response.add_result();
316      info.set_name(macro->ReadableName());
317      info.set_kind(SymbolKind::kFunction);
318      info.location().SetTo(macro->Position());
319    } else if (symbol->IsBuiltin()) {
320      Builtin* builtin = Builtin::cast(symbol);
321      SymbolInformation info = response.add_result();
322      info.set_name(builtin->ReadableName());
323      info.set_kind(SymbolKind::kFunction);
324      info.location().SetTo(builtin->Position());
325    } else if (symbol->IsGenericCallable()) {
326      GenericCallable* generic = GenericCallable::cast(symbol);
327      SymbolInformation info = response.add_result();
328      info.set_name(generic->name());
329      info.set_kind(SymbolKind::kFunction);
330      info.location().SetTo(generic->Position());
331    } else if (symbol->IsTypeAlias()) {
332      const Type* type = TypeAlias::cast(symbol)->type();
333      SymbolKind kind =
334          type->IsClassType() ? SymbolKind::kClass : SymbolKind::kStruct;
335
336      SymbolInformation sym = response.add_result();
337      sym.set_name(type->ToString());
338      sym.set_kind(kind);
339      sym.location().SetTo(symbol->Position());
340    }
341  }
342
343  // Trigger empty array creation in case no symbols were found.
344  USE(response.result_size());
345
346  writer(std::move(response.GetJsonValue()));
347}
348
349}  // namespace
350
351void HandleMessage(JsonValue raw_message, MessageWriter writer) {
352  Request<bool> request(std::move(raw_message));
353
354  // We ignore responses for now. They are matched to requests
355  // by id and don't have a method set.
356  // TODO(szuend): Implement proper response handling for requests
357  //               that originate from the server.
358  if (!request.has_method()) {
359    Logger::Log("[info] Unhandled response with id ", request.id(), "\n\n");
360    return;
361  }
362
363  const std::string method = request.method();
364  if (method == "initialize") {
365    HandleInitializeRequest(
366        InitializeRequest(std::move(request.GetJsonValue())), writer);
367  } else if (method == "initialized") {
368    HandleInitializedNotification(writer);
369  } else if (method == "torque/fileList") {
370    HandleTorqueFileListNotification(
371        TorqueFileListNotification(std::move(request.GetJsonValue())), writer);
372  } else if (method == "textDocument/definition") {
373    HandleGotoDefinitionRequest(
374        GotoDefinitionRequest(std::move(request.GetJsonValue())), writer);
375  } else if (method == "workspace/didChangeWatchedFiles") {
376    HandleChangeWatchedFilesNotification(
377        DidChangeWatchedFilesNotification(std::move(request.GetJsonValue())),
378        writer);
379  } else if (method == "textDocument/documentSymbol") {
380    HandleDocumentSymbolRequest(
381        DocumentSymbolRequest(std::move(request.GetJsonValue())), writer);
382  } else {
383    Logger::Log("[error] Message of type ", method, " is not handled!\n\n");
384  }
385}
386
387}  // namespace ls
388}  // namespace torque
389}  // namespace internal
390}  // namespace v8
391