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
16 namespace v8 {
17 namespace internal {
18 namespace torque {
19
20 DEFINE_CONTEXTUAL_VARIABLE(Logger)
21 DEFINE_CONTEXTUAL_VARIABLE(TorqueFileList)
22 DEFINE_CONTEXTUAL_VARIABLE(DiagnosticsFiles)
23
24 namespace ls {
25
26 static const char kContentLength[] = "Content-Length: ";
27 static 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.
31 constexpr const char* kProtocolLineEnding = "\n\n";
32 #else
33 constexpr const char* kProtocolLineEnding = "\r\n\r\n";
34 #endif
35
ReadMessage()36 JsonValue 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
WriteMessage(JsonValue message)56 void 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
65 namespace {
66
ResetCompilationErrorDiagnostics(MessageWriter writer)67 void 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).
87 class DiagnosticCollector {
88 public:
AddTorqueMessage(const TorqueMessage & message)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
notifications()106 std::map<SourceId, PublishDiagnosticsNotification>& notifications() {
107 return notifications_;
108 }
109
110 private:
GetOrCreateNotificationForSource(SourceId id)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
ShouldAddMessageOfKind(TorqueMessage::Kind kind)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
PopulateRangeFromSourcePosition(Range range,const SourcePosition & position)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
ServerityFor(TorqueMessage::Kind kind)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
SendCompilationDiagnostics(const TorqueCompilerResult & result,MessageWriter writer)160 void 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
CompilationFinished(TorqueCompilerResult result,MessageWriter writer)182 void 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
189 namespace {
190
RecompileTorque(MessageWriter writer)191 void 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
RecompileTorqueWithDiagnostics(MessageWriter writer)206 void RecompileTorqueWithDiagnostics(MessageWriter writer) {
207 ResetCompilationErrorDiagnostics(writer);
208 RecompileTorque(writer);
209 }
210
HandleInitializeRequest(InitializeRequest request,MessageWriter writer)211 void 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
HandleInitializedNotification(MessageWriter writer)228 void 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
HandleTorqueFileListNotification(TorqueFileListNotification notification,MessageWriter writer)247 void 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
HandleGotoDefinitionRequest(GotoDefinitionRequest request,MessageWriter writer)266 void 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
HandleChangeWatchedFilesNotification(DidChangeWatchedFilesNotification notification,MessageWriter writer)296 void 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
HandleDocumentSymbolRequest(DocumentSymbolRequest request,MessageWriter writer)303 void 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
HandleMessage(JsonValue raw_message,MessageWriter writer)351 void 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