aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_lsp_server/src/cargo_check.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_lsp_server/src/cargo_check.rs')
-rw-r--r--crates/ra_lsp_server/src/cargo_check.rs533
1 files changed, 533 insertions, 0 deletions
diff --git a/crates/ra_lsp_server/src/cargo_check.rs b/crates/ra_lsp_server/src/cargo_check.rs
new file mode 100644
index 000000000..d5ff02154
--- /dev/null
+++ b/crates/ra_lsp_server/src/cargo_check.rs
@@ -0,0 +1,533 @@
1use cargo_metadata::{
2 diagnostic::{
3 Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
4 DiagnosticSpanMacroExpansion,
5 },
6 Message,
7};
8use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender, TryRecvError};
9use lsp_types::{
10 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
11 NumberOrString, Position, Range, Url,
12};
13use parking_lot::RwLock;
14use std::{
15 collections::HashMap,
16 fmt::Write,
17 path::PathBuf,
18 process::{Command, Stdio},
19 sync::Arc,
20 thread::JoinHandle,
21 time::Instant,
22};
23
24#[derive(Debug)]
25pub struct CheckWatcher {
26 pub task_recv: Receiver<CheckTask>,
27 pub cmd_send: Sender<CheckCommand>,
28 pub shared: Arc<RwLock<CheckWatcherSharedState>>,
29 handle: JoinHandle<()>,
30}
31
32impl CheckWatcher {
33 pub fn new(workspace_root: PathBuf) -> CheckWatcher {
34 let shared = Arc::new(RwLock::new(CheckWatcherSharedState::new()));
35
36 let (task_send, task_recv) = unbounded::<CheckTask>();
37 let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
38 let shared_ = shared.clone();
39 let handle = std::thread::spawn(move || {
40 let mut check = CheckWatcherState::new(shared_, workspace_root);
41 check.run(&task_send, &cmd_recv);
42 });
43
44 CheckWatcher { task_recv, cmd_send, handle, shared }
45 }
46
47 pub fn update(&self) {
48 self.cmd_send.send(CheckCommand::Update).unwrap();
49 }
50}
51
52pub struct CheckWatcherState {
53 workspace_root: PathBuf,
54 running: bool,
55 watcher: WatchThread,
56 last_update_req: Option<Instant>,
57 shared: Arc<RwLock<CheckWatcherSharedState>>,
58}
59
60#[derive(Debug)]
61pub struct CheckWatcherSharedState {
62 diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
63 suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
64}
65
66impl CheckWatcherSharedState {
67 fn new() -> CheckWatcherSharedState {
68 CheckWatcherSharedState {
69 diagnostic_collection: HashMap::new(),
70 suggested_fix_collection: HashMap::new(),
71 }
72 }
73
74 pub fn clear(&mut self, task_send: &Sender<CheckTask>) {
75 let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect();
76
77 self.diagnostic_collection.clear();
78 self.suggested_fix_collection.clear();
79
80 for uri in cleared_files {
81 task_send.send(CheckTask::Update(uri.clone())).unwrap();
82 }
83 }
84
85 pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> {
86 self.diagnostic_collection.get(uri).map(|d| d.as_slice())
87 }
88
89 pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> {
90 self.suggested_fix_collection.get(uri).map(|d| d.as_slice())
91 }
92
93 fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) {
94 let diagnostics = self.diagnostic_collection.entry(file_uri).or_default();
95
96 // If we're building multiple targets it's possible we've already seen this diagnostic
97 let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic));
98 if is_duplicate {
99 return;
100 }
101
102 diagnostics.push(diagnostic);
103 }
104
105 fn add_suggested_fix_for_diagnostic(
106 &mut self,
107 mut suggested_fix: SuggestedFix,
108 diagnostic: &Diagnostic,
109 ) {
110 let file_uri = suggested_fix.location.uri.clone();
111 let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default();
112
113 let existing_suggestion: Option<&mut SuggestedFix> =
114 file_suggestions.iter_mut().find(|s| s == &&suggested_fix);
115 if let Some(existing_suggestion) = existing_suggestion {
116 // The existing suggestion also applies to this new diagnostic
117 existing_suggestion.diagnostics.push(diagnostic.clone());
118 } else {
119 // We haven't seen this suggestion before
120 suggested_fix.diagnostics.push(diagnostic.clone());
121 file_suggestions.push(suggested_fix);
122 }
123 }
124}
125
126#[derive(Debug)]
127pub enum CheckTask {
128 Update(Url),
129}
130
131pub enum CheckCommand {
132 Update,
133}
134
135impl CheckWatcherState {
136 pub fn new(
137 shared: Arc<RwLock<CheckWatcherSharedState>>,
138 workspace_root: PathBuf,
139 ) -> CheckWatcherState {
140 let watcher = WatchThread::new(&workspace_root);
141 CheckWatcherState { workspace_root, running: false, watcher, last_update_req: None, shared }
142 }
143
144 pub fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
145 self.running = true;
146 while self.running {
147 select! {
148 recv(&cmd_recv) -> cmd => match cmd {
149 Ok(cmd) => self.handle_command(cmd),
150 Err(RecvError) => {
151 // Command channel has closed, so shut down
152 self.running = false;
153 },
154 },
155 recv(self.watcher.message_recv) -> msg => match msg {
156 Ok(msg) => self.handle_message(msg, task_send),
157 Err(RecvError) => {},
158 }
159 };
160
161 if self.should_recheck() {
162 self.last_update_req.take();
163 self.shared.write().clear(task_send);
164
165 self.watcher.cancel();
166 self.watcher = WatchThread::new(&self.workspace_root);
167 }
168 }
169 }
170
171 fn should_recheck(&mut self) -> bool {
172 if let Some(_last_update_req) = &self.last_update_req {
173 // We currently only request an update on save, as we need up to
174 // date source on disk for cargo check to do it's magic, so we
175 // don't really need to debounce the requests at this point.
176 return true;
177 }
178 false
179 }
180
181 fn handle_command(&mut self, cmd: CheckCommand) {
182 match cmd {
183 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
184 }
185 }
186
187 fn handle_message(&mut self, msg: cargo_metadata::Message, task_send: &Sender<CheckTask>) {
188 match msg {
189 Message::CompilerArtifact(_msg) => {
190 // TODO: Status display
191 }
192
193 Message::CompilerMessage(msg) => {
194 let map_result =
195 match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) {
196 Some(map_result) => map_result,
197 None => return,
198 };
199
200 let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result;
201 let file_uri = location.uri.clone();
202
203 if !suggested_fixes.is_empty() {
204 for suggested_fix in suggested_fixes {
205 self.shared
206 .write()
207 .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic);
208 }
209 }
210 self.shared.write().add_diagnostic(file_uri, diagnostic);
211
212 task_send.send(CheckTask::Update(location.uri)).unwrap();
213 }
214
215 Message::BuildScriptExecuted(_msg) => {}
216 Message::Unknown => {}
217 }
218 }
219}
220
221/// WatchThread exists to wrap around the communication needed to be able to
222/// run `cargo check` without blocking. Currently the Rust standard library
223/// doesn't provide a way to read sub-process output without blocking, so we
224/// have to wrap sub-processes output handling in a thread and pass messages
225/// back over a channel.
226struct WatchThread {
227 message_recv: Receiver<cargo_metadata::Message>,
228 cancel_send: Sender<()>,
229}
230
231impl WatchThread {
232 fn new(workspace_root: &PathBuf) -> WatchThread {
233 let manifest_path = format!("{}/Cargo.toml", workspace_root.to_string_lossy());
234 let (message_send, message_recv) = unbounded();
235 let (cancel_send, cancel_recv) = unbounded();
236 std::thread::spawn(move || {
237 let mut command = Command::new("cargo")
238 .args(&["check", "--message-format=json", "--manifest-path", &manifest_path])
239 .stdout(Stdio::piped())
240 .stderr(Stdio::null())
241 .spawn()
242 .expect("couldn't launch cargo");
243
244 for message in cargo_metadata::parse_messages(command.stdout.take().unwrap()) {
245 match cancel_recv.try_recv() {
246 Ok(()) | Err(TryRecvError::Disconnected) => {
247 command.kill().expect("couldn't kill command");
248 }
249 Err(TryRecvError::Empty) => (),
250 }
251
252 message_send.send(message.unwrap()).unwrap();
253 }
254 });
255 WatchThread { message_recv, cancel_send }
256 }
257
258 fn cancel(&self) {
259 let _ = self.cancel_send.send(());
260 }
261}
262
263/// Converts a Rust level string to a LSP severity
264fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
265 match val {
266 DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error),
267 DiagnosticLevel::Error => Some(DiagnosticSeverity::Error),
268 DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning),
269 DiagnosticLevel::Note => Some(DiagnosticSeverity::Information),
270 DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint),
271 DiagnosticLevel::Unknown => None,
272 }
273}
274
275/// Check whether a file name is from macro invocation
276fn is_from_macro(file_name: &str) -> bool {
277 file_name.starts_with('<') && file_name.ends_with('>')
278}
279
280/// Converts a Rust macro span to a LSP location recursively
281fn map_macro_span_to_location(
282 span_macro: &DiagnosticSpanMacroExpansion,
283 workspace_root: &PathBuf,
284) -> Option<Location> {
285 if !is_from_macro(&span_macro.span.file_name) {
286 return Some(map_span_to_location(&span_macro.span, workspace_root));
287 }
288
289 if let Some(expansion) = &span_macro.span.expansion {
290 return map_macro_span_to_location(&expansion, workspace_root);
291 }
292
293 None
294}
295
296/// Converts a Rust span to a LSP location
297fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
298 if is_from_macro(&span.file_name) && span.expansion.is_some() {
299 let expansion = span.expansion.as_ref().unwrap();
300 if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) {
301 return macro_range;
302 }
303 }
304
305 let mut file_name = workspace_root.clone();
306 file_name.push(&span.file_name);
307 let uri = Url::from_file_path(file_name).unwrap();
308
309 let range = Range::new(
310 Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1),
311 Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1),
312 );
313
314 Location { uri, range }
315}
316
317/// Converts a secondary Rust span to a LSP related information
318///
319/// If the span is unlabelled this will return `None`.
320fn map_secondary_span_to_related(
321 span: &DiagnosticSpan,
322 workspace_root: &PathBuf,
323) -> Option<DiagnosticRelatedInformation> {
324 if let Some(label) = &span.label {
325 let location = map_span_to_location(span, workspace_root);
326 Some(DiagnosticRelatedInformation { location, message: label.clone() })
327 } else {
328 // Nothing to label this with
329 None
330 }
331}
332
333/// Determines if diagnostic is related to unused code
334fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool {
335 if let Some(code) = &rd.code {
336 match code.code.as_str() {
337 "dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes"
338 | "unused_imports" | "unused_macros" | "unused_variables" => true,
339 _ => false,
340 }
341 } else {
342 false
343 }
344}
345
346/// Determines if diagnostic is related to deprecated code
347fn is_deprecated(rd: &RustDiagnostic) -> bool {
348 if let Some(code) = &rd.code {
349 match code.code.as_str() {
350 "deprecated" => true,
351 _ => false,
352 }
353 } else {
354 false
355 }
356}
357
358#[derive(Debug)]
359pub struct SuggestedFix {
360 pub title: String,
361 pub location: Location,
362 pub replacement: String,
363 pub applicability: Applicability,
364 pub diagnostics: Vec<Diagnostic>,
365}
366
367impl std::cmp::PartialEq<SuggestedFix> for SuggestedFix {
368 fn eq(&self, other: &SuggestedFix) -> bool {
369 if self.title == other.title
370 && self.location == other.location
371 && self.replacement == other.replacement
372 {
373 // Applicability doesn't impl PartialEq...
374 match (&self.applicability, &other.applicability) {
375 (Applicability::MachineApplicable, Applicability::MachineApplicable) => true,
376 (Applicability::HasPlaceholders, Applicability::HasPlaceholders) => true,
377 (Applicability::MaybeIncorrect, Applicability::MaybeIncorrect) => true,
378 (Applicability::Unspecified, Applicability::Unspecified) => true,
379 _ => false,
380 }
381 } else {
382 false
383 }
384 }
385}
386
387enum MappedRustChildDiagnostic {
388 Related(DiagnosticRelatedInformation),
389 SuggestedFix(SuggestedFix),
390 MessageLine(String),
391}
392
393fn map_rust_child_diagnostic(
394 rd: &RustDiagnostic,
395 workspace_root: &PathBuf,
396) -> MappedRustChildDiagnostic {
397 let span: &DiagnosticSpan = match rd.spans.iter().find(|s| s.is_primary) {
398 Some(span) => span,
399 None => {
400 // `rustc` uses these spanless children as a way to print multi-line
401 // messages
402 return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
403 }
404 };
405
406 // If we have a primary span use its location, otherwise use the parent
407 let location = map_span_to_location(&span, workspace_root);
408
409 if let Some(suggested_replacement) = &span.suggested_replacement {
410 // Include our replacement in the title unless it's empty
411 let title = if !suggested_replacement.is_empty() {
412 format!("{}: '{}'", rd.message, suggested_replacement)
413 } else {
414 rd.message.clone()
415 };
416
417 MappedRustChildDiagnostic::SuggestedFix(SuggestedFix {
418 title,
419 location,
420 replacement: suggested_replacement.clone(),
421 applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown),
422 diagnostics: vec![],
423 })
424 } else {
425 MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
426 location,
427 message: rd.message.clone(),
428 })
429 }
430}
431
432struct MappedRustDiagnostic {
433 location: Location,
434 diagnostic: Diagnostic,
435 suggested_fixes: Vec<SuggestedFix>,
436}
437
438/// Converts a Rust root diagnostic to LSP form
439///
440/// This flattens the Rust diagnostic by:
441///
442/// 1. Creating a LSP diagnostic with the root message and primary span.
443/// 2. Adding any labelled secondary spans to `relatedInformation`
444/// 3. Categorising child diagnostics as either `SuggestedFix`es,
445/// `relatedInformation` or additional message lines.
446///
447/// If the diagnostic has no primary span this will return `None`
448fn map_rust_diagnostic_to_lsp(
449 rd: &RustDiagnostic,
450 workspace_root: &PathBuf,
451) -> Option<MappedRustDiagnostic> {
452 let primary_span = rd.spans.iter().find(|s| s.is_primary)?;
453
454 let location = map_span_to_location(&primary_span, workspace_root);
455
456 let severity = map_level_to_severity(rd.level);
457 let mut primary_span_label = primary_span.label.as_ref();
458
459 let mut source = String::from("rustc");
460 let mut code = rd.code.as_ref().map(|c| c.code.clone());
461 if let Some(code_val) = &code {
462 // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
463 let scoped_code: Vec<&str> = code_val.split("::").collect();
464 if scoped_code.len() == 2 {
465 source = String::from(scoped_code[0]);
466 code = Some(String::from(scoped_code[1]));
467 }
468 }
469
470 let mut related_information = vec![];
471 let mut tags = vec![];
472
473 for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) {
474 let related = map_secondary_span_to_related(secondary_span, workspace_root);
475 if let Some(related) = related {
476 related_information.push(related);
477 }
478 }
479
480 let mut suggested_fixes = vec![];
481 let mut message = rd.message.clone();
482 for child in &rd.children {
483 let child = map_rust_child_diagnostic(&child, workspace_root);
484 match child {
485 MappedRustChildDiagnostic::Related(related) => related_information.push(related),
486 MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => {
487 suggested_fixes.push(suggested_fix)
488 }
489 MappedRustChildDiagnostic::MessageLine(message_line) => {
490 write!(&mut message, "\n{}", message_line).unwrap();
491
492 // These secondary messages usually duplicate the content of the
493 // primary span label.
494 primary_span_label = None;
495 }
496 }
497 }
498
499 if let Some(primary_span_label) = primary_span_label {
500 write!(&mut message, "\n{}", primary_span_label).unwrap();
501 }
502
503 if is_unused_or_unnecessary(rd) {
504 tags.push(DiagnosticTag::Unnecessary);
505 }
506
507 if is_deprecated(rd) {
508 tags.push(DiagnosticTag::Deprecated);
509 }
510
511 let diagnostic = Diagnostic {
512 range: location.range,
513 severity,
514 code: code.map(NumberOrString::String),
515 source: Some(source),
516 message: rd.message.clone(),
517 related_information: if !related_information.is_empty() {
518 Some(related_information)
519 } else {
520 None
521 },
522 tags: if !tags.is_empty() { Some(tags) } else { None },
523 };
524
525 Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes })
526}
527
528fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
529 left.source == right.source
530 && left.severity == right.severity
531 && left.range == right.range
532 && left.message == right.message
533}