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