2019-12-27 10:10:07 +00:00
|
|
|
//! This module provides the functionality needed to convert diagnostics from
|
|
|
|
//! `cargo check` json format to the LSP diagnostic format.
|
|
|
|
use cargo_metadata::diagnostic::{
|
2020-01-31 18:23:25 +00:00
|
|
|
Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan, DiagnosticSpanMacroExpansion,
|
2019-12-27 10:10:07 +00:00
|
|
|
};
|
|
|
|
use lsp_types::{
|
2020-01-31 18:23:25 +00:00
|
|
|
CodeAction, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
|
|
|
Location, NumberOrString, Position, Range, TextEdit, Url, WorkspaceEdit,
|
2019-12-27 10:10:07 +00:00
|
|
|
};
|
2019-12-29 17:27:14 +00:00
|
|
|
use std::{
|
2020-03-12 14:01:53 +00:00
|
|
|
collections::HashMap,
|
2019-12-29 17:27:14 +00:00
|
|
|
fmt::Write,
|
|
|
|
path::{Component, Path, PathBuf, Prefix},
|
|
|
|
str::FromStr,
|
|
|
|
};
|
2019-12-27 10:10:07 +00:00
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test;
|
|
|
|
|
|
|
|
/// Converts a Rust level string to a LSP severity
|
|
|
|
fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
|
|
|
|
match val {
|
|
|
|
DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error),
|
|
|
|
DiagnosticLevel::Error => Some(DiagnosticSeverity::Error),
|
|
|
|
DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning),
|
|
|
|
DiagnosticLevel::Note => Some(DiagnosticSeverity::Information),
|
|
|
|
DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint),
|
|
|
|
DiagnosticLevel::Unknown => None,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Check whether a file name is from macro invocation
|
|
|
|
fn is_from_macro(file_name: &str) -> bool {
|
|
|
|
file_name.starts_with('<') && file_name.ends_with('>')
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts a Rust macro span to a LSP location recursively
|
|
|
|
fn map_macro_span_to_location(
|
|
|
|
span_macro: &DiagnosticSpanMacroExpansion,
|
|
|
|
workspace_root: &PathBuf,
|
|
|
|
) -> Option<Location> {
|
|
|
|
if !is_from_macro(&span_macro.span.file_name) {
|
|
|
|
return Some(map_span_to_location(&span_macro.span, workspace_root));
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(expansion) = &span_macro.span.expansion {
|
|
|
|
return map_macro_span_to_location(&expansion, workspace_root);
|
|
|
|
}
|
|
|
|
|
|
|
|
None
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:45:54 +00:00
|
|
|
/// Converts a Rust span to a LSP location, resolving macro expansion site if neccesary
|
2019-12-27 10:10:07 +00:00
|
|
|
fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
|
2019-12-29 18:14:18 +00:00
|
|
|
if span.expansion.is_some() {
|
2019-12-27 10:10:07 +00:00
|
|
|
let expansion = span.expansion.as_ref().unwrap();
|
|
|
|
if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) {
|
|
|
|
return macro_range;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-29 18:45:54 +00:00
|
|
|
map_span_to_location_naive(span, workspace_root)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts a Rust span to a LSP location
|
|
|
|
fn map_span_to_location_naive(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
|
2019-12-27 10:10:07 +00:00
|
|
|
let mut file_name = workspace_root.clone();
|
|
|
|
file_name.push(&span.file_name);
|
2019-12-29 17:27:14 +00:00
|
|
|
let uri = url_from_path_with_drive_lowercasing(file_name).unwrap();
|
2019-12-27 10:10:07 +00:00
|
|
|
|
|
|
|
let range = Range::new(
|
|
|
|
Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1),
|
|
|
|
Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1),
|
|
|
|
);
|
|
|
|
|
|
|
|
Location { uri, range }
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts a secondary Rust span to a LSP related information
|
|
|
|
///
|
|
|
|
/// If the span is unlabelled this will return `None`.
|
|
|
|
fn map_secondary_span_to_related(
|
|
|
|
span: &DiagnosticSpan,
|
|
|
|
workspace_root: &PathBuf,
|
|
|
|
) -> Option<DiagnosticRelatedInformation> {
|
|
|
|
if let Some(label) = &span.label {
|
|
|
|
let location = map_span_to_location(span, workspace_root);
|
|
|
|
Some(DiagnosticRelatedInformation { location, message: label.clone() })
|
|
|
|
} else {
|
|
|
|
// Nothing to label this with
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines if diagnostic is related to unused code
|
|
|
|
fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool {
|
|
|
|
if let Some(code) = &rd.code {
|
|
|
|
match code.code.as_str() {
|
|
|
|
"dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes"
|
|
|
|
| "unused_imports" | "unused_macros" | "unused_variables" => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Determines if diagnostic is related to deprecated code
|
|
|
|
fn is_deprecated(rd: &RustDiagnostic) -> bool {
|
|
|
|
if let Some(code) = &rd.code {
|
|
|
|
match code.code.as_str() {
|
|
|
|
"deprecated" => true,
|
|
|
|
_ => false,
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum MappedRustChildDiagnostic {
|
|
|
|
Related(DiagnosticRelatedInformation),
|
2020-01-31 18:23:25 +00:00
|
|
|
SuggestedFix(CodeAction),
|
2019-12-27 10:10:07 +00:00
|
|
|
MessageLine(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
fn map_rust_child_diagnostic(
|
|
|
|
rd: &RustDiagnostic,
|
|
|
|
workspace_root: &PathBuf,
|
|
|
|
) -> MappedRustChildDiagnostic {
|
2020-03-12 14:01:53 +00:00
|
|
|
let spans: Vec<&DiagnosticSpan> = rd.spans.iter().filter(|s| s.is_primary).collect();
|
|
|
|
if spans.is_empty() {
|
|
|
|
// `rustc` uses these spanless children as a way to print multi-line
|
|
|
|
// messages
|
|
|
|
return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut edit_map: HashMap<Url, Vec<TextEdit>> = HashMap::new();
|
|
|
|
for &span in &spans {
|
|
|
|
if let Some(suggested_replacement) = &span.suggested_replacement {
|
|
|
|
let location = map_span_to_location(span, workspace_root);
|
|
|
|
let edit = TextEdit::new(location.range, suggested_replacement.clone());
|
|
|
|
edit_map.entry(location.uri).or_default().push(edit);
|
2019-12-27 10:10:07 +00:00
|
|
|
}
|
2020-03-12 14:01:53 +00:00
|
|
|
}
|
2020-01-31 18:23:25 +00:00
|
|
|
|
2020-03-12 14:01:53 +00:00
|
|
|
if !edit_map.is_empty() {
|
2020-01-31 18:23:25 +00:00
|
|
|
MappedRustChildDiagnostic::SuggestedFix(CodeAction {
|
2020-03-12 14:01:53 +00:00
|
|
|
title: rd.message.clone(),
|
2020-01-31 18:23:25 +00:00
|
|
|
kind: Some("quickfix".to_string()),
|
|
|
|
diagnostics: None,
|
2020-03-12 14:01:53 +00:00
|
|
|
edit: Some(WorkspaceEdit::new(edit_map)),
|
2020-01-31 18:23:25 +00:00
|
|
|
command: None,
|
|
|
|
is_preferred: None,
|
2019-12-27 10:10:07 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
|
2020-03-12 14:01:53 +00:00
|
|
|
location: map_span_to_location(spans[0], workspace_root),
|
2019-12-27 10:10:07 +00:00
|
|
|
message: rd.message.clone(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
pub(crate) struct MappedRustDiagnostic {
|
|
|
|
pub location: Location,
|
|
|
|
pub diagnostic: Diagnostic,
|
2020-01-31 18:23:25 +00:00
|
|
|
pub fixes: Vec<CodeAction>,
|
2019-12-27 10:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Converts a Rust root diagnostic to LSP form
|
|
|
|
///
|
|
|
|
/// This flattens the Rust diagnostic by:
|
|
|
|
///
|
|
|
|
/// 1. Creating a LSP diagnostic with the root message and primary span.
|
|
|
|
/// 2. Adding any labelled secondary spans to `relatedInformation`
|
|
|
|
/// 3. Categorising child diagnostics as either `SuggestedFix`es,
|
|
|
|
/// `relatedInformation` or additional message lines.
|
|
|
|
///
|
|
|
|
/// If the diagnostic has no primary span this will return `None`
|
|
|
|
pub(crate) fn map_rust_diagnostic_to_lsp(
|
|
|
|
rd: &RustDiagnostic,
|
|
|
|
workspace_root: &PathBuf,
|
2020-03-12 14:24:20 +00:00
|
|
|
) -> Vec<MappedRustDiagnostic> {
|
|
|
|
let primary_spans: Vec<&DiagnosticSpan> = rd.spans.iter().filter(|s| s.is_primary).collect();
|
|
|
|
if primary_spans.is_empty() {
|
|
|
|
return vec![];
|
|
|
|
}
|
2019-12-27 10:10:07 +00:00
|
|
|
|
|
|
|
let severity = map_level_to_severity(rd.level);
|
|
|
|
|
|
|
|
let mut source = String::from("rustc");
|
|
|
|
let mut code = rd.code.as_ref().map(|c| c.code.clone());
|
|
|
|
if let Some(code_val) = &code {
|
|
|
|
// See if this is an RFC #2103 scoped lint (e.g. from Clippy)
|
|
|
|
let scoped_code: Vec<&str> = code_val.split("::").collect();
|
|
|
|
if scoped_code.len() == 2 {
|
|
|
|
source = String::from(scoped_code[0]);
|
|
|
|
code = Some(String::from(scoped_code[1]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-12 14:24:20 +00:00
|
|
|
let mut needs_primary_span_label = true;
|
2019-12-27 10:10:07 +00:00
|
|
|
let mut related_information = vec![];
|
|
|
|
let mut tags = vec![];
|
|
|
|
|
|
|
|
for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) {
|
|
|
|
let related = map_secondary_span_to_related(secondary_span, workspace_root);
|
|
|
|
if let Some(related) = related {
|
|
|
|
related_information.push(related);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-31 18:23:25 +00:00
|
|
|
let mut fixes = vec![];
|
2019-12-27 10:10:07 +00:00
|
|
|
let mut message = rd.message.clone();
|
|
|
|
for child in &rd.children {
|
|
|
|
let child = map_rust_child_diagnostic(&child, workspace_root);
|
|
|
|
match child {
|
|
|
|
MappedRustChildDiagnostic::Related(related) => related_information.push(related),
|
2020-02-18 12:53:02 +00:00
|
|
|
MappedRustChildDiagnostic::SuggestedFix(code_action) => fixes.push(code_action),
|
2019-12-27 10:10:07 +00:00
|
|
|
MappedRustChildDiagnostic::MessageLine(message_line) => {
|
|
|
|
write!(&mut message, "\n{}", message_line).unwrap();
|
|
|
|
|
|
|
|
// These secondary messages usually duplicate the content of the
|
|
|
|
// primary span label.
|
2020-03-12 14:24:20 +00:00
|
|
|
needs_primary_span_label = false;
|
2019-12-27 10:10:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if is_unused_or_unnecessary(rd) {
|
|
|
|
tags.push(DiagnosticTag::Unnecessary);
|
|
|
|
}
|
|
|
|
|
|
|
|
if is_deprecated(rd) {
|
|
|
|
tags.push(DiagnosticTag::Deprecated);
|
|
|
|
}
|
|
|
|
|
2020-03-12 14:24:20 +00:00
|
|
|
primary_spans
|
|
|
|
.iter()
|
|
|
|
.map(|primary_span| {
|
|
|
|
let location = map_span_to_location(&primary_span, workspace_root);
|
|
|
|
|
|
|
|
let mut message = message.clone();
|
|
|
|
if needs_primary_span_label {
|
|
|
|
if let Some(primary_span_label) = &primary_span.label {
|
|
|
|
write!(&mut message, "\n{}", primary_span_label).unwrap();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If error occurs from macro expansion, add related info pointing to
|
|
|
|
// where the error originated
|
|
|
|
if !is_from_macro(&primary_span.file_name) && primary_span.expansion.is_some() {
|
|
|
|
let def_loc = map_span_to_location_naive(&primary_span, workspace_root);
|
|
|
|
related_information.push(DiagnosticRelatedInformation {
|
|
|
|
location: def_loc,
|
|
|
|
message: "Error originated from macro here".to_string(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let diagnostic = Diagnostic {
|
|
|
|
range: location.range,
|
|
|
|
severity,
|
|
|
|
code: code.clone().map(NumberOrString::String),
|
|
|
|
source: Some(source.clone()),
|
|
|
|
message,
|
|
|
|
related_information: if !related_information.is_empty() {
|
|
|
|
Some(related_information.clone())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
tags: if !tags.is_empty() { Some(tags.clone()) } else { None },
|
|
|
|
};
|
|
|
|
|
|
|
|
MappedRustDiagnostic { location, diagnostic, fixes: fixes.clone() }
|
|
|
|
})
|
|
|
|
.collect()
|
2019-12-27 10:10:07 +00:00
|
|
|
}
|
2019-12-29 17:27:14 +00:00
|
|
|
|
|
|
|
/// Returns a `Url` object from a given path, will lowercase drive letters if present.
|
|
|
|
/// This will only happen when processing windows paths.
|
|
|
|
///
|
|
|
|
/// When processing non-windows path, this is essentially the same as `Url::from_file_path`.
|
|
|
|
pub fn url_from_path_with_drive_lowercasing(
|
|
|
|
path: impl AsRef<Path>,
|
|
|
|
) -> Result<Url, Box<dyn std::error::Error + Send + Sync>> {
|
|
|
|
let component_has_windows_drive = path.as_ref().components().any(|comp| {
|
|
|
|
if let Component::Prefix(c) = comp {
|
|
|
|
match c.kind() {
|
|
|
|
Prefix::Disk(_) | Prefix::VerbatimDisk(_) => return true,
|
|
|
|
_ => return false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
|
|
|
});
|
|
|
|
|
|
|
|
// VSCode expects drive letters to be lowercased, where rust will uppercase the drive letters.
|
|
|
|
if component_has_windows_drive {
|
|
|
|
let url_original = Url::from_file_path(&path)
|
|
|
|
.map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?;
|
|
|
|
|
|
|
|
let drive_partition: Vec<&str> = url_original.as_str().rsplitn(2, ':').collect();
|
|
|
|
|
|
|
|
// There is a drive partition, but we never found a colon.
|
|
|
|
// This should not happen, but in this case we just pass it through.
|
|
|
|
if drive_partition.len() == 1 {
|
|
|
|
return Ok(url_original);
|
|
|
|
}
|
|
|
|
|
|
|
|
let joined = drive_partition[1].to_ascii_lowercase() + ":" + drive_partition[0];
|
|
|
|
let url = Url::from_str(&joined).expect("This came from a valid `Url`");
|
|
|
|
|
|
|
|
Ok(url)
|
|
|
|
} else {
|
|
|
|
Ok(Url::from_file_path(&path)
|
|
|
|
.map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// `Url` is not able to parse windows paths on unix machines.
|
|
|
|
#[cfg(target_os = "windows")]
|
|
|
|
#[cfg(test)]
|
|
|
|
mod path_conversion_windows_tests {
|
|
|
|
use super::url_from_path_with_drive_lowercasing;
|
|
|
|
#[test]
|
|
|
|
fn test_lowercase_drive_letter_with_drive() {
|
|
|
|
let url = url_from_path_with_drive_lowercasing("C:\\Test").unwrap();
|
|
|
|
|
|
|
|
assert_eq!(url.to_string(), "file:///c:/Test");
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_drive_without_colon_passthrough() {
|
|
|
|
let url = url_from_path_with_drive_lowercasing(r#"\\localhost\C$\my_dir"#).unwrap();
|
|
|
|
|
|
|
|
assert_eq!(url.to_string(), "file://localhost/C$/my_dir");
|
|
|
|
}
|
|
|
|
}
|