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