cargo.rs

  1use std::{
  2    path::{Component, Path, Prefix},
  3    process::Stdio,
  4    sync::atomic::{self, AtomicUsize},
  5};
  6
  7use cargo_metadata::{
  8    Message,
  9    diagnostic::{Applicability, Diagnostic as CargoDiagnostic, DiagnosticLevel, DiagnosticSpan},
 10};
 11use collections::HashMap;
 12use gpui::{AppContext, Entity, Task};
 13use itertools::Itertools as _;
 14use language::Diagnostic;
 15use project::{
 16    Worktree, lsp_store::rust_analyzer_ext::CARGO_DIAGNOSTICS_SOURCE_NAME,
 17    project_settings::ProjectSettings,
 18};
 19use serde::{Deserialize, Serialize};
 20use settings::Settings;
 21use smol::{
 22    channel::Receiver,
 23    io::{AsyncBufReadExt, BufReader},
 24    process::Command,
 25};
 26use ui::App;
 27use util::ResultExt;
 28
 29use crate::ProjectDiagnosticsEditor;
 30
 31#[derive(Debug, serde::Deserialize)]
 32#[serde(untagged)]
 33enum CargoMessage {
 34    Cargo(Message),
 35    Rustc(CargoDiagnostic),
 36}
 37
 38/// Appends formatted string to a `String`.
 39macro_rules! format_to {
 40    ($buf:expr) => ();
 41    ($buf:expr, $lit:literal $($arg:tt)*) => {
 42        {
 43            use ::std::fmt::Write as _;
 44            // We can't do ::std::fmt::Write::write_fmt($buf, format_args!($lit $($arg)*))
 45            // unfortunately, as that loses out on autoref behavior.
 46            _ = $buf.write_fmt(format_args!($lit $($arg)*))
 47        }
 48    };
 49}
 50
 51pub fn cargo_diagnostics_sources(
 52    editor: &ProjectDiagnosticsEditor,
 53    cx: &App,
 54) -> Vec<Entity<Worktree>> {
 55    let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
 56        .diagnostics
 57        .fetch_cargo_diagnostics();
 58    if !fetch_cargo_diagnostics {
 59        return Vec::new();
 60    }
 61    editor
 62        .project
 63        .read(cx)
 64        .worktrees(cx)
 65        .filter(|worktree| worktree.read(cx).entry_for_path("Cargo.toml").is_some())
 66        .collect()
 67}
 68
 69#[derive(Debug)]
 70pub enum FetchUpdate {
 71    Diagnostic(CargoDiagnostic),
 72    Progress(String),
 73}
 74
 75#[derive(Debug)]
 76pub enum FetchStatus {
 77    Started,
 78    Progress { message: String },
 79    Finished,
 80}
 81
 82pub fn fetch_worktree_diagnostics(
 83    worktree_root: &Path,
 84    cx: &App,
 85) -> Option<(Task<()>, Receiver<FetchUpdate>)> {
 86    let diagnostics_settings = ProjectSettings::get_global(cx)
 87        .diagnostics
 88        .cargo
 89        .as_ref()
 90        .filter(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics)?;
 91    let command_string = diagnostics_settings
 92        .diagnostics_fetch_command
 93        .iter()
 94        .join(" ");
 95    let mut command_parts = diagnostics_settings.diagnostics_fetch_command.iter();
 96    let mut command = Command::new(command_parts.next()?)
 97        .args(command_parts)
 98        .envs(diagnostics_settings.env.clone())
 99        .current_dir(worktree_root)
100        .stdout(Stdio::piped())
101        .stderr(Stdio::null())
102        .kill_on_drop(true)
103        .spawn()
104        .log_err()?;
105
106    let stdout = command.stdout.take()?;
107    let mut reader = BufReader::new(stdout);
108    let (tx, rx) = smol::channel::unbounded();
109    let error_threshold = 10;
110
111    let cargo_diagnostics_fetch_task = cx.background_spawn(async move {
112        let _command = command;
113        let mut errors = 0;
114        loop {
115            let mut line = String::new();
116            match reader.read_line(&mut line).await {
117                Ok(0) => {
118                    return;
119                },
120                Ok(_) => {
121                    errors = 0;
122                    let mut deserializer = serde_json::Deserializer::from_str(&line);
123                    deserializer.disable_recursion_limit();
124                    let send_result = match CargoMessage::deserialize(&mut deserializer) {
125                        Ok(CargoMessage::Cargo(Message::CompilerMessage(message))) => tx.send(FetchUpdate::Diagnostic(message.message)).await,
126                        Ok(CargoMessage::Cargo(Message::CompilerArtifact(artifact))) => tx.send(FetchUpdate::Progress(format!("Compiled {:?}", artifact.manifest_path.parent().unwrap_or(&artifact.manifest_path)))).await,
127                        Ok(CargoMessage::Cargo(_)) => Ok(()),
128                        Ok(CargoMessage::Rustc(rustc_message)) =>  tx.send(FetchUpdate::Diagnostic(rustc_message)).await,
129                        Err(_) => {
130                            log::debug!("Failed to parse cargo diagnostics from line '{line}'");
131                            Ok(())
132                        },
133                    };
134                    if send_result.is_err() {
135                        return;
136                    }
137                },
138                Err(e) => {
139                    log::error!("Failed to read line from {command_string} command output when fetching cargo diagnostics: {e}");
140                    errors += 1;
141                    if errors >= error_threshold {
142                        log::error!("Failed {error_threshold} times, aborting the diagnostics fetch");
143                        return;
144                    }
145                },
146            }
147        }
148    });
149
150    Some((cargo_diagnostics_fetch_task, rx))
151}
152
153static CARGO_DIAGNOSTICS_FETCH_GENERATION: AtomicUsize = AtomicUsize::new(0);
154
155#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
156struct CargoFetchDiagnosticData {
157    generation: usize,
158}
159
160pub fn next_cargo_fetch_generation() {
161    CARGO_DIAGNOSTICS_FETCH_GENERATION.fetch_add(1, atomic::Ordering::Release);
162}
163
164pub fn is_outdated_cargo_fetch_diagnostic(diagnostic: &Diagnostic) -> bool {
165    if let Some(data) = diagnostic
166        .data
167        .clone()
168        .and_then(|data| serde_json::from_value::<CargoFetchDiagnosticData>(data).ok())
169    {
170        let current_generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire);
171        data.generation < current_generation
172    } else {
173        false
174    }
175}
176
177/// Converts a Rust root diagnostic to LSP form
178///
179/// This flattens the Rust diagnostic by:
180///
181/// 1. Creating a LSP diagnostic with the root message and primary span.
182/// 2. Adding any labelled secondary spans to `relatedInformation`
183/// 3. Categorising child diagnostics as either `SuggestedFix`es,
184///    `relatedInformation` or additional message lines.
185///
186/// If the diagnostic has no primary span this will return `None`
187///
188/// Taken from https://github.com/rust-lang/rust-analyzer/blob/fe7b4f2ad96f7c13cc571f45edc2c578b35dddb4/crates/rust-analyzer/src/diagnostics/to_proto.rs#L275-L285
189pub(crate) fn map_rust_diagnostic_to_lsp(
190    worktree_root: &Path,
191    cargo_diagnostic: &CargoDiagnostic,
192) -> Vec<(lsp::Url, lsp::Diagnostic)> {
193    let primary_spans: Vec<&DiagnosticSpan> = cargo_diagnostic
194        .spans
195        .iter()
196        .filter(|s| s.is_primary)
197        .collect();
198    if primary_spans.is_empty() {
199        return Vec::new();
200    }
201
202    let severity = diagnostic_severity(cargo_diagnostic.level);
203
204    let mut source = String::from(CARGO_DIAGNOSTICS_SOURCE_NAME);
205    let mut code = cargo_diagnostic.code.as_ref().map(|c| c.code.clone());
206
207    if let Some(code_val) = &code {
208        // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
209        let scoped_code: Vec<&str> = code_val.split("::").collect();
210        if scoped_code.len() == 2 {
211            source = String::from(scoped_code[0]);
212            code = Some(String::from(scoped_code[1]));
213        }
214    }
215
216    let mut needs_primary_span_label = true;
217    let mut subdiagnostics = Vec::new();
218    let mut tags = Vec::new();
219
220    for secondary_span in cargo_diagnostic.spans.iter().filter(|s| !s.is_primary) {
221        if let Some(label) = secondary_span.label.clone() {
222            subdiagnostics.push(lsp::DiagnosticRelatedInformation {
223                location: location(worktree_root, secondary_span),
224                message: label,
225            });
226        }
227    }
228
229    let mut message = cargo_diagnostic.message.clone();
230    for child in &cargo_diagnostic.children {
231        let child = map_rust_child_diagnostic(worktree_root, child);
232        match child {
233            MappedRustChildDiagnostic::SubDiagnostic(sub) => {
234                subdiagnostics.push(sub);
235            }
236            MappedRustChildDiagnostic::MessageLine(message_line) => {
237                format_to!(message, "\n{message_line}");
238
239                // These secondary messages usually duplicate the content of the
240                // primary span label.
241                needs_primary_span_label = false;
242            }
243        }
244    }
245
246    if let Some(code) = &cargo_diagnostic.code {
247        let code = code.code.as_str();
248        if matches!(
249            code,
250            "dead_code"
251                | "unknown_lints"
252                | "unreachable_code"
253                | "unused_attributes"
254                | "unused_imports"
255                | "unused_macros"
256                | "unused_variables"
257        ) {
258            tags.push(lsp::DiagnosticTag::UNNECESSARY);
259        }
260
261        if matches!(code, "deprecated") {
262            tags.push(lsp::DiagnosticTag::DEPRECATED);
263        }
264    }
265
266    let code_description = match source.as_str() {
267        "rustc" => rustc_code_description(code.as_deref()),
268        "clippy" => clippy_code_description(code.as_deref()),
269        _ => None,
270    };
271
272    let generation = CARGO_DIAGNOSTICS_FETCH_GENERATION.load(atomic::Ordering::Acquire);
273    let data = Some(
274        serde_json::to_value(CargoFetchDiagnosticData { generation })
275            .expect("Serializing a regular Rust struct"),
276    );
277
278    primary_spans
279        .iter()
280        .flat_map(|primary_span| {
281            let primary_location = primary_location(worktree_root, primary_span);
282            let message = {
283                let mut message = message.clone();
284                if needs_primary_span_label {
285                    if let Some(primary_span_label) = &primary_span.label {
286                        format_to!(message, "\n{primary_span_label}");
287                    }
288                }
289                message
290            };
291            // Each primary diagnostic span may result in multiple LSP diagnostics.
292            let mut diagnostics = Vec::new();
293
294            let mut related_info_macro_calls = vec![];
295
296            // If error occurs from macro expansion, add related info pointing to
297            // where the error originated
298            // Also, we would generate an additional diagnostic, so that exact place of macro
299            // will be highlighted in the error origin place.
300            let span_stack = std::iter::successors(Some(*primary_span), |span| {
301                Some(&span.expansion.as_ref()?.span)
302            });
303            for (i, span) in span_stack.enumerate() {
304                if is_dummy_macro_file(&span.file_name) {
305                    continue;
306                }
307
308                // First span is the original diagnostic, others are macro call locations that
309                // generated that code.
310                let is_in_macro_call = i != 0;
311
312                let secondary_location = location(worktree_root, span);
313                if secondary_location == primary_location {
314                    continue;
315                }
316                related_info_macro_calls.push(lsp::DiagnosticRelatedInformation {
317                    location: secondary_location.clone(),
318                    message: if is_in_macro_call {
319                        "Error originated from macro call here".to_owned()
320                    } else {
321                        "Actual error occurred here".to_owned()
322                    },
323                });
324                // For the additional in-macro diagnostic we add the inverse message pointing to the error location in code.
325                let information_for_additional_diagnostic =
326                    vec![lsp::DiagnosticRelatedInformation {
327                        location: primary_location.clone(),
328                        message: "Exact error occurred here".to_owned(),
329                    }];
330
331                let diagnostic = lsp::Diagnostic {
332                    range: secondary_location.range,
333                    // downgrade to hint if we're pointing at the macro
334                    severity: Some(lsp::DiagnosticSeverity::HINT),
335                    code: code.clone().map(lsp::NumberOrString::String),
336                    code_description: code_description.clone(),
337                    source: Some(source.clone()),
338                    message: message.clone(),
339                    related_information: Some(information_for_additional_diagnostic),
340                    tags: if tags.is_empty() {
341                        None
342                    } else {
343                        Some(tags.clone())
344                    },
345                    data: data.clone(),
346                };
347                diagnostics.push((secondary_location.uri, diagnostic));
348            }
349
350            // Emit the primary diagnostic.
351            diagnostics.push((
352                primary_location.uri.clone(),
353                lsp::Diagnostic {
354                    range: primary_location.range,
355                    severity,
356                    code: code.clone().map(lsp::NumberOrString::String),
357                    code_description: code_description.clone(),
358                    source: Some(source.clone()),
359                    message,
360                    related_information: {
361                        let info = related_info_macro_calls
362                            .iter()
363                            .cloned()
364                            .chain(subdiagnostics.iter().cloned())
365                            .collect::<Vec<_>>();
366                        if info.is_empty() { None } else { Some(info) }
367                    },
368                    tags: if tags.is_empty() {
369                        None
370                    } else {
371                        Some(tags.clone())
372                    },
373                    data: data.clone(),
374                },
375            ));
376
377            // Emit hint-level diagnostics for all `related_information` entries such as "help"s.
378            // This is useful because they will show up in the user's editor, unlike
379            // `related_information`, which just produces hard-to-read links, at least in VS Code.
380            let back_ref = lsp::DiagnosticRelatedInformation {
381                location: primary_location,
382                message: "original diagnostic".to_owned(),
383            };
384            for sub in &subdiagnostics {
385                diagnostics.push((
386                    sub.location.uri.clone(),
387                    lsp::Diagnostic {
388                        range: sub.location.range,
389                        severity: Some(lsp::DiagnosticSeverity::HINT),
390                        code: code.clone().map(lsp::NumberOrString::String),
391                        code_description: code_description.clone(),
392                        source: Some(source.clone()),
393                        message: sub.message.clone(),
394                        related_information: Some(vec![back_ref.clone()]),
395                        tags: None, // don't apply modifiers again
396                        data: data.clone(),
397                    },
398                ));
399            }
400
401            diagnostics
402        })
403        .collect()
404}
405
406fn rustc_code_description(code: Option<&str>) -> Option<lsp::CodeDescription> {
407    code.filter(|code| {
408        let mut chars = code.chars();
409        chars.next() == Some('E')
410            && chars.by_ref().take(4).all(|c| c.is_ascii_digit())
411            && chars.next().is_none()
412    })
413    .and_then(|code| {
414        lsp::Url::parse(&format!(
415            "https://doc.rust-lang.org/error-index.html#{code}"
416        ))
417        .ok()
418        .map(|href| lsp::CodeDescription { href })
419    })
420}
421
422fn clippy_code_description(code: Option<&str>) -> Option<lsp::CodeDescription> {
423    code.and_then(|code| {
424        lsp::Url::parse(&format!(
425            "https://rust-lang.github.io/rust-clippy/master/index.html#{code}"
426        ))
427        .ok()
428        .map(|href| lsp::CodeDescription { href })
429    })
430}
431
432/// Determines the LSP severity from a diagnostic
433fn diagnostic_severity(level: DiagnosticLevel) -> Option<lsp::DiagnosticSeverity> {
434    let res = match level {
435        DiagnosticLevel::Ice => lsp::DiagnosticSeverity::ERROR,
436        DiagnosticLevel::Error => lsp::DiagnosticSeverity::ERROR,
437        DiagnosticLevel::Warning => lsp::DiagnosticSeverity::WARNING,
438        DiagnosticLevel::Note => lsp::DiagnosticSeverity::INFORMATION,
439        DiagnosticLevel::Help => lsp::DiagnosticSeverity::HINT,
440        _ => return None,
441    };
442    Some(res)
443}
444
445enum MappedRustChildDiagnostic {
446    SubDiagnostic(lsp::DiagnosticRelatedInformation),
447    MessageLine(String),
448}
449
450fn map_rust_child_diagnostic(
451    worktree_root: &Path,
452    cargo_diagnostic: &CargoDiagnostic,
453) -> MappedRustChildDiagnostic {
454    let spans: Vec<&DiagnosticSpan> = cargo_diagnostic
455        .spans
456        .iter()
457        .filter(|s| s.is_primary)
458        .collect();
459    if spans.is_empty() {
460        // `rustc` uses these spanless children as a way to print multi-line
461        // messages
462        return MappedRustChildDiagnostic::MessageLine(cargo_diagnostic.message.clone());
463    }
464
465    let mut edit_map: HashMap<lsp::Url, Vec<lsp::TextEdit>> = HashMap::default();
466    let mut suggested_replacements = Vec::new();
467    for &span in &spans {
468        if let Some(suggested_replacement) = &span.suggested_replacement {
469            if !suggested_replacement.is_empty() {
470                suggested_replacements.push(suggested_replacement);
471            }
472            let location = location(worktree_root, span);
473            let edit = lsp::TextEdit::new(location.range, suggested_replacement.clone());
474
475            // Only actually emit a quickfix if the suggestion is "valid enough".
476            // We accept both "MaybeIncorrect" and "MachineApplicable". "MaybeIncorrect" means that
477            // the suggestion is *complete* (contains no placeholders where code needs to be
478            // inserted), but might not be what the user wants, or might need minor adjustments.
479            if matches!(
480                span.suggestion_applicability,
481                None | Some(Applicability::MaybeIncorrect | Applicability::MachineApplicable)
482            ) {
483                edit_map.entry(location.uri).or_default().push(edit);
484            }
485        }
486    }
487
488    // rustc renders suggestion diagnostics by appending the suggested replacement, so do the same
489    // here, otherwise the diagnostic text is missing useful information.
490    let mut message = cargo_diagnostic.message.clone();
491    if !suggested_replacements.is_empty() {
492        message.push_str(": ");
493        let suggestions = suggested_replacements
494            .iter()
495            .map(|suggestion| format!("`{suggestion}`"))
496            .join(", ");
497        message.push_str(&suggestions);
498    }
499
500    MappedRustChildDiagnostic::SubDiagnostic(lsp::DiagnosticRelatedInformation {
501        location: location(worktree_root, spans[0]),
502        message,
503    })
504}
505
506/// Converts a Rust span to a LSP location
507fn location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location {
508    let file_name = worktree_root.join(&span.file_name);
509    let uri = url_from_abs_path(&file_name);
510
511    let range = {
512        lsp::Range::new(
513            position(span, span.line_start, span.column_start.saturating_sub(1)),
514            position(span, span.line_end, span.column_end.saturating_sub(1)),
515        )
516    };
517    lsp::Location::new(uri, range)
518}
519
520/// Returns a `Url` object from a given path, will lowercase drive letters if present.
521/// This will only happen when processing windows paths.
522///
523/// When processing non-windows path, this is essentially the same as `Url::from_file_path`.
524pub(crate) fn url_from_abs_path(path: &Path) -> lsp::Url {
525    let url = lsp::Url::from_file_path(path).unwrap();
526    match path.components().next() {
527        Some(Component::Prefix(prefix))
528            if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_)) =>
529        {
530            // Need to lowercase driver letter
531        }
532        _ => return url,
533    }
534
535    let driver_letter_range = {
536        let (scheme, drive_letter, _rest) = match url.as_str().splitn(3, ':').collect_tuple() {
537            Some(it) => it,
538            None => return url,
539        };
540        let start = scheme.len() + ':'.len_utf8();
541        start..(start + drive_letter.len())
542    };
543
544    // Note: lowercasing the `path` itself doesn't help, the `Url::parse`
545    // machinery *also* canonicalizes the drive letter. So, just massage the
546    // string in place.
547    let mut url: String = url.into();
548    url[driver_letter_range].make_ascii_lowercase();
549    lsp::Url::parse(&url).unwrap()
550}
551
552fn position(
553    span: &DiagnosticSpan,
554    line_number: usize,
555    column_offset_utf32: usize,
556) -> lsp::Position {
557    let line_index = line_number - span.line_start;
558
559    let column_offset_encoded = match span.text.get(line_index) {
560        // Fast path.
561        Some(line) if line.text.is_ascii() => column_offset_utf32,
562        Some(line) => {
563            let line_prefix_len = line
564                .text
565                .char_indices()
566                .take(column_offset_utf32)
567                .last()
568                .map(|(pos, c)| pos + c.len_utf8())
569                .unwrap_or(0);
570            let line_prefix = &line.text[..line_prefix_len];
571            line_prefix.len()
572        }
573        None => column_offset_utf32,
574    };
575
576    lsp::Position {
577        line: (line_number as u32).saturating_sub(1),
578        character: column_offset_encoded as u32,
579    }
580}
581
582/// Checks whether a file name is from macro invocation and does not refer to an actual file.
583fn is_dummy_macro_file(file_name: &str) -> bool {
584    file_name.starts_with('<') && file_name.ends_with('>')
585}
586
587/// Extracts a suitable "primary" location from a rustc diagnostic.
588///
589/// This takes locations pointing into the standard library, or generally outside the current
590/// workspace into account and tries to avoid those, in case macros are involved.
591fn primary_location(worktree_root: &Path, span: &DiagnosticSpan) -> lsp::Location {
592    let span_stack = std::iter::successors(Some(span), |span| Some(&span.expansion.as_ref()?.span));
593    for span in span_stack.clone() {
594        let abs_path = worktree_root.join(&span.file_name);
595        if !is_dummy_macro_file(&span.file_name) && abs_path.starts_with(worktree_root) {
596            return location(worktree_root, span);
597        }
598    }
599
600    // Fall back to the outermost macro invocation if no suitable span comes up.
601    let last_span = span_stack.last().unwrap();
602    location(worktree_root, last_span)
603}