1 use crate::world::Options;
4 Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
5 DiagnosticSpanMacroExpansion,
9 use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender, TryRecvError};
11 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
12 NumberOrString, Position, Range, Url,
14 use parking_lot::RwLock;
19 process::{Command, Stdio},
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<()>,
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()));
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 || {
44 CheckWatcherState::new(check_command, check_args, workspace_root, shared_);
45 check.run(&task_send, &cmd_recv);
48 CheckWatcher { task_recv, cmd_send, handle, shared }
51 pub fn update(&self) {
52 self.cmd_send.send(CheckCommand::Update).unwrap();
56 pub struct CheckWatcherState {
57 check_command: Option<String>,
58 check_args: Vec<String>,
59 workspace_root: PathBuf,
62 last_update_req: Option<Instant>,
63 shared: Arc<RwLock<CheckWatcherSharedState>>,
67 pub struct CheckWatcherSharedState {
68 diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
69 suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
72 impl CheckWatcherSharedState {
73 fn new() -> CheckWatcherSharedState {
74 CheckWatcherSharedState {
75 diagnostic_collection: HashMap::new(),
76 suggested_fix_collection: HashMap::new(),
80 pub fn clear(&mut self, task_send: &Sender<CheckTask>) {
81 let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect();
83 self.diagnostic_collection.clear();
84 self.suggested_fix_collection.clear();
86 for uri in cleared_files {
87 task_send.send(CheckTask::Update(uri.clone())).unwrap();
91 pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> {
92 self.diagnostic_collection.get(uri).map(|d| d.as_slice())
95 pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> {
96 self.suggested_fix_collection.get(uri).map(|d| d.as_slice())
99 fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) {
100 let diagnostics = self.diagnostic_collection.entry(file_uri).or_default();
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));
108 diagnostics.push(diagnostic);
111 fn add_suggested_fix_for_diagnostic(
113 mut suggested_fix: SuggestedFix,
114 diagnostic: &Diagnostic,
116 let file_uri = suggested_fix.location.uri.clone();
117 let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default();
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());
125 // We haven't seen this suggestion before
126 suggested_fix.diagnostics.push(diagnostic.clone());
127 file_suggestions.push(suggested_fix);
137 pub enum CheckCommand {
141 impl CheckWatcherState {
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);
155 last_update_req: None,
160 pub fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
164 recv(&cmd_recv) -> cmd => match cmd {
165 Ok(cmd) => self.handle_command(cmd),
167 // Command channel has closed, so shut down
168 self.running = false;
171 recv(self.watcher.message_recv) -> msg => match msg {
172 Ok(msg) => self.handle_message(msg, task_send),
173 Err(RecvError) => {},
177 if self.should_recheck() {
178 self.last_update_req.take();
179 self.shared.write().clear(task_send);
181 self.watcher.cancel();
182 self.watcher = WatchThread::new(
183 self.check_command.as_ref(),
185 &self.workspace_root,
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.
201 fn handle_command(&mut self, cmd: CheckCommand) {
203 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
207 fn handle_message(&mut self, msg: cargo_metadata::Message, task_send: &Sender<CheckTask>) {
209 Message::CompilerArtifact(_msg) => {
210 // TODO: Status display
213 Message::CompilerMessage(msg) => {
215 match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) {
216 Some(map_result) => map_result,
220 let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result;
221 let file_uri = location.uri.clone();
223 if !suggested_fixes.is_empty() {
224 for suggested_fix in suggested_fixes {
227 .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic);
230 self.shared.write().add_diagnostic(file_uri, diagnostic);
232 task_send.send(CheckTask::Update(location.uri)).unwrap();
235 Message::BuildScriptExecuted(_msg) => {}
236 Message::Unknown => {}
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.
247 message_recv: Receiver<cargo_metadata::Message>,
248 cancel_send: Sender<()>,
253 check_command: Option<&String>,
254 check_args: &[String],
255 workspace_root: &PathBuf,
257 let check_command = check_command.cloned().unwrap_or("check".to_string());
258 let mut args: Vec<String> = vec![
260 "--message-format=json".to_string(),
261 "--manifest-path".to_string(),
262 format!("{}/Cargo.toml", workspace_root.to_string_lossy()),
264 args.extend(check_args.iter().cloned());
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")
271 .stdout(Stdio::piped())
272 .stderr(Stdio::null())
274 .expect("couldn't launch cargo");
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");
281 Err(TryRecvError::Empty) => (),
284 message_send.send(message.unwrap()).unwrap();
287 WatchThread { message_recv, cancel_send }
291 let _ = self.cancel_send.send(());
295 /// Converts a Rust level string to a LSP severity
296 fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
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,
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('>')
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));
321 if let Some(expansion) = &span_macro.span.expansion {
322 return map_macro_span_to_location(&expansion, workspace_root);
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) {
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();
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),
346 Location { uri, range }
349 /// Converts a secondary Rust span to a LSP related information
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() })
360 // Nothing to label this with
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,
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,
391 pub struct SuggestedFix {
393 pub location: Location,
394 pub replacement: String,
395 pub applicability: Applicability,
396 pub diagnostics: Vec<Diagnostic>,
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
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,
419 enum MappedRustChildDiagnostic {
420 Related(DiagnosticRelatedInformation),
421 SuggestedFix(SuggestedFix),
425 fn map_rust_child_diagnostic(
427 workspace_root: &PathBuf,
428 ) -> MappedRustChildDiagnostic {
429 let span: &DiagnosticSpan = match rd.spans.iter().find(|s| s.is_primary) {
432 // `rustc` uses these spanless children as a way to print multi-line
434 return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
438 // If we have a primary span use its location, otherwise use the parent
439 let location = map_span_to_location(&span, workspace_root);
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)
449 MappedRustChildDiagnostic::SuggestedFix(SuggestedFix {
452 replacement: suggested_replacement.clone(),
453 applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown),
457 MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
459 message: rd.message.clone(),
464 struct MappedRustDiagnostic {
466 diagnostic: Diagnostic,
467 suggested_fixes: Vec<SuggestedFix>,
470 /// Converts a Rust root diagnostic to LSP form
472 /// This flattens the Rust diagnostic by:
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.
479 /// If the diagnostic has no primary span this will return `None`
480 fn map_rust_diagnostic_to_lsp(
482 workspace_root: &PathBuf,
483 ) -> Option<MappedRustDiagnostic> {
484 let primary_span = rd.spans.iter().find(|s| s.is_primary)?;
486 let location = map_span_to_location(&primary_span, workspace_root);
488 let severity = map_level_to_severity(rd.level);
489 let mut primary_span_label = primary_span.label.as_ref();
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]));
502 let mut related_information = vec![];
503 let mut tags = vec![];
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);
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);
517 MappedRustChildDiagnostic::Related(related) => related_information.push(related),
518 MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => {
519 suggested_fixes.push(suggested_fix)
521 MappedRustChildDiagnostic::MessageLine(message_line) => {
522 write!(&mut message, "\n{}", message_line).unwrap();
524 // These secondary messages usually duplicate the content of the
525 // primary span label.
526 primary_span_label = None;
531 if let Some(primary_span_label) = primary_span_label {
532 write!(&mut message, "\n{}", primary_span_label).unwrap();
535 if is_unused_or_unnecessary(rd) {
536 tags.push(DiagnosticTag::Unnecessary);
539 if is_deprecated(rd) {
540 tags.push(DiagnosticTag::Deprecated);
543 let diagnostic = Diagnostic {
544 range: location.range,
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)
554 tags: if !tags.is_empty() { Some(tags) } else { None },
557 Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes })
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