aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_flycheck/src/conv.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_flycheck/src/conv.rs')
-rw-r--r--crates/ra_flycheck/src/conv.rs341
1 files changed, 341 insertions, 0 deletions
diff --git a/crates/ra_flycheck/src/conv.rs b/crates/ra_flycheck/src/conv.rs
new file mode 100644
index 000000000..817543deb
--- /dev/null
+++ b/crates/ra_flycheck/src/conv.rs
@@ -0,0 +1,341 @@
1//! This module provides the functionality needed to convert diagnostics from
2//! `cargo check` json format to the LSP diagnostic format.
3use cargo_metadata::diagnostic::{
4 Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
5 DiagnosticSpanMacroExpansion,
6};
7use lsp_types::{
8 CodeAction, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
9 Location, NumberOrString, Position, Range, TextEdit, Url, WorkspaceEdit,
10};
11use std::{
12 collections::HashMap,
13 fmt::Write,
14 path::{Component, Path, PathBuf, Prefix},
15 str::FromStr,
16};
17
18#[cfg(test)]
19mod test;
20
21/// Converts a Rust level string to a LSP severity
22fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
23 match val {
24 DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error),
25 DiagnosticLevel::Error => Some(DiagnosticSeverity::Error),
26 DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning),
27 DiagnosticLevel::Note => Some(DiagnosticSeverity::Information),
28 DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint),
29 DiagnosticLevel::Unknown => None,
30 }
31}
32
33/// Check whether a file name is from macro invocation
34fn is_from_macro(file_name: &str) -> bool {
35 file_name.starts_with('<') && file_name.ends_with('>')
36}
37
38/// Converts a Rust macro span to a LSP location recursively
39fn map_macro_span_to_location(
40 span_macro: &DiagnosticSpanMacroExpansion,
41 workspace_root: &PathBuf,
42) -> Option<Location> {
43 if !is_from_macro(&span_macro.span.file_name) {
44 return Some(map_span_to_location(&span_macro.span, workspace_root));
45 }
46
47 if let Some(expansion) = &span_macro.span.expansion {
48 return map_macro_span_to_location(&expansion, workspace_root);
49 }
50
51 None
52}
53
54/// Converts a Rust span to a LSP location, resolving macro expansion site if neccesary
55fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
56 if span.expansion.is_some() {
57 let expansion = span.expansion.as_ref().unwrap();
58 if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) {
59 return macro_range;
60 }
61 }
62
63 map_span_to_location_naive(span, workspace_root)
64}
65
66/// Converts a Rust span to a LSP location
67fn map_span_to_location_naive(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
68 let mut file_name = workspace_root.clone();
69 file_name.push(&span.file_name);
70 let uri = url_from_path_with_drive_lowercasing(file_name).unwrap();
71
72 let range = Range::new(
73 Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1),
74 Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1),
75 );
76
77 Location { uri, range }
78}
79
80/// Converts a secondary Rust span to a LSP related information
81///
82/// If the span is unlabelled this will return `None`.
83fn map_secondary_span_to_related(
84 span: &DiagnosticSpan,
85 workspace_root: &PathBuf,
86) -> Option<DiagnosticRelatedInformation> {
87 if let Some(label) = &span.label {
88 let location = map_span_to_location(span, workspace_root);
89 Some(DiagnosticRelatedInformation { location, message: label.clone() })
90 } else {
91 // Nothing to label this with
92 None
93 }
94}
95
96/// Determines if diagnostic is related to unused code
97fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool {
98 if let Some(code) = &rd.code {
99 match code.code.as_str() {
100 "dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes"
101 | "unused_imports" | "unused_macros" | "unused_variables" => true,
102 _ => false,
103 }
104 } else {
105 false
106 }
107}
108
109/// Determines if diagnostic is related to deprecated code
110fn is_deprecated(rd: &RustDiagnostic) -> bool {
111 if let Some(code) = &rd.code {
112 match code.code.as_str() {
113 "deprecated" => true,
114 _ => false,
115 }
116 } else {
117 false
118 }
119}
120
121enum MappedRustChildDiagnostic {
122 Related(DiagnosticRelatedInformation),
123 SuggestedFix(CodeAction),
124 MessageLine(String),
125}
126
127fn map_rust_child_diagnostic(
128 rd: &RustDiagnostic,
129 workspace_root: &PathBuf,
130) -> MappedRustChildDiagnostic {
131 let spans: Vec<&DiagnosticSpan> = rd.spans.iter().filter(|s| s.is_primary).collect();
132 if spans.is_empty() {
133 // `rustc` uses these spanless children as a way to print multi-line
134 // messages
135 return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
136 }
137
138 let mut edit_map: HashMap<Url, Vec<TextEdit>> = HashMap::new();
139 for &span in &spans {
140 match (&span.suggestion_applicability, &span.suggested_replacement) {
141 (Some(Applicability::MachineApplicable), Some(suggested_replacement)) => {
142 let location = map_span_to_location(span, workspace_root);
143 let edit = TextEdit::new(location.range, suggested_replacement.clone());
144 edit_map.entry(location.uri).or_default().push(edit);
145 }
146 _ => {}
147 }
148 }
149
150 if !edit_map.is_empty() {
151 MappedRustChildDiagnostic::SuggestedFix(CodeAction {
152 title: rd.message.clone(),
153 kind: Some("quickfix".to_string()),
154 diagnostics: None,
155 edit: Some(WorkspaceEdit::new(edit_map)),
156 command: None,
157 is_preferred: None,
158 })
159 } else {
160 MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
161 location: map_span_to_location(spans[0], workspace_root),
162 message: rd.message.clone(),
163 })
164 }
165}
166
167#[derive(Debug)]
168pub(crate) struct MappedRustDiagnostic {
169 pub location: Location,
170 pub diagnostic: Diagnostic,
171 pub fixes: Vec<CodeAction>,
172}
173
174/// Converts a Rust root diagnostic to LSP form
175///
176/// This flattens the Rust diagnostic by:
177///
178/// 1. Creating a LSP diagnostic with the root message and primary span.
179/// 2. Adding any labelled secondary spans to `relatedInformation`
180/// 3. Categorising child diagnostics as either `SuggestedFix`es,
181/// `relatedInformation` or additional message lines.
182///
183/// If the diagnostic has no primary span this will return `None`
184pub(crate) fn map_rust_diagnostic_to_lsp(
185 rd: &RustDiagnostic,
186 workspace_root: &PathBuf,
187) -> Vec<MappedRustDiagnostic> {
188 let primary_spans: Vec<&DiagnosticSpan> = rd.spans.iter().filter(|s| s.is_primary).collect();
189 if primary_spans.is_empty() {
190 return vec![];
191 }
192
193 let severity = map_level_to_severity(rd.level);
194
195 let mut source = String::from("rustc");
196 let mut code = rd.code.as_ref().map(|c| c.code.clone());
197 if let Some(code_val) = &code {
198 // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
199 let scoped_code: Vec<&str> = code_val.split("::").collect();
200 if scoped_code.len() == 2 {
201 source = String::from(scoped_code[0]);
202 code = Some(String::from(scoped_code[1]));
203 }
204 }
205
206 let mut needs_primary_span_label = true;
207 let mut related_information = vec![];
208 let mut tags = vec![];
209
210 for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) {
211 let related = map_secondary_span_to_related(secondary_span, workspace_root);
212 if let Some(related) = related {
213 related_information.push(related);
214 }
215 }
216
217 let mut fixes = vec![];
218 let mut message = rd.message.clone();
219 for child in &rd.children {
220 let child = map_rust_child_diagnostic(&child, workspace_root);
221 match child {
222 MappedRustChildDiagnostic::Related(related) => related_information.push(related),
223 MappedRustChildDiagnostic::SuggestedFix(code_action) => fixes.push(code_action),
224 MappedRustChildDiagnostic::MessageLine(message_line) => {
225 write!(&mut message, "\n{}", message_line).unwrap();
226
227 // These secondary messages usually duplicate the content of the
228 // primary span label.
229 needs_primary_span_label = false;
230 }
231 }
232 }
233
234 if is_unused_or_unnecessary(rd) {
235 tags.push(DiagnosticTag::Unnecessary);
236 }
237
238 if is_deprecated(rd) {
239 tags.push(DiagnosticTag::Deprecated);
240 }
241
242 primary_spans
243 .iter()
244 .map(|primary_span| {
245 let location = map_span_to_location(&primary_span, workspace_root);
246
247 let mut message = message.clone();
248 if needs_primary_span_label {
249 if let Some(primary_span_label) = &primary_span.label {
250 write!(&mut message, "\n{}", primary_span_label).unwrap();
251 }
252 }
253
254 // If error occurs from macro expansion, add related info pointing to
255 // where the error originated
256 if !is_from_macro(&primary_span.file_name) && primary_span.expansion.is_some() {
257 let def_loc = map_span_to_location_naive(&primary_span, workspace_root);
258 related_information.push(DiagnosticRelatedInformation {
259 location: def_loc,
260 message: "Error originated from macro here".to_string(),
261 });
262 }
263
264 let diagnostic = Diagnostic {
265 range: location.range,
266 severity,
267 code: code.clone().map(NumberOrString::String),
268 source: Some(source.clone()),
269 message,
270 related_information: if !related_information.is_empty() {
271 Some(related_information.clone())
272 } else {
273 None
274 },
275 tags: if !tags.is_empty() { Some(tags.clone()) } else { None },
276 };
277
278 MappedRustDiagnostic { location, diagnostic, fixes: fixes.clone() }
279 })
280 .collect()
281}
282
283/// Returns a `Url` object from a given path, will lowercase drive letters if present.
284/// This will only happen when processing windows paths.
285///
286/// When processing non-windows path, this is essentially the same as `Url::from_file_path`.
287pub fn url_from_path_with_drive_lowercasing(
288 path: impl AsRef<Path>,
289) -> Result<Url, Box<dyn std::error::Error + Send + Sync>> {
290 let component_has_windows_drive = path.as_ref().components().any(|comp| {
291 if let Component::Prefix(c) = comp {
292 match c.kind() {
293 Prefix::Disk(_) | Prefix::VerbatimDisk(_) => return true,
294 _ => return false,
295 }
296 }
297 false
298 });
299
300 // VSCode expects drive letters to be lowercased, where rust will uppercase the drive letters.
301 if component_has_windows_drive {
302 let url_original = Url::from_file_path(&path)
303 .map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?;
304
305 let drive_partition: Vec<&str> = url_original.as_str().rsplitn(2, ':').collect();
306
307 // There is a drive partition, but we never found a colon.
308 // This should not happen, but in this case we just pass it through.
309 if drive_partition.len() == 1 {
310 return Ok(url_original);
311 }
312
313 let joined = drive_partition[1].to_ascii_lowercase() + ":" + drive_partition[0];
314 let url = Url::from_str(&joined).expect("This came from a valid `Url`");
315
316 Ok(url)
317 } else {
318 Ok(Url::from_file_path(&path)
319 .map_err(|_| format!("can't convert path to url: {}", path.as_ref().display()))?)
320 }
321}
322
323// `Url` is not able to parse windows paths on unix machines.
324#[cfg(target_os = "windows")]
325#[cfg(test)]
326mod path_conversion_windows_tests {
327 use super::url_from_path_with_drive_lowercasing;
328 #[test]
329 fn test_lowercase_drive_letter_with_drive() {
330 let url = url_from_path_with_drive_lowercasing("C:\\Test").unwrap();
331
332 assert_eq!(url.to_string(), "file:///c:/Test");
333 }
334
335 #[test]
336 fn test_drive_without_colon_passthrough() {
337 let url = url_from_path_with_drive_lowercasing(r#"\\localhost\C$\my_dir"#).unwrap();
338
339 assert_eq!(url.to_string(), "file://localhost/C$/my_dir");
340 }
341}