aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_cargo_watch/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_cargo_watch/src')
-rw-r--r--crates/ra_cargo_watch/src/conv.rs359
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_clippy_pass_by_ref.snap85
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_handles_macro_location.snap46
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_macro_compiler_error.snap61
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_incompatible_type_for_trait.snap46
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_mismatched_type.snap46
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_unused_variable.snap70
-rw-r--r--crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_wrong_number_of_parameters.snap65
-rw-r--r--crates/ra_cargo_watch/src/conv/test.rs929
-rw-r--r--crates/ra_cargo_watch/src/lib.rs394
10 files changed, 2101 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}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_clippy_pass_by_ref.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_clippy_pass_by_ref.snap
new file mode 100644
index 000000000..cb0920914
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_clippy_pass_by_ref.snap
@@ -0,0 +1,85 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/compiler/mir/tagset.rs",
8 range: Range {
9 start: Position {
10 line: 41,
11 character: 23,
12 },
13 end: Position {
14 line: 41,
15 character: 28,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 41,
23 character: 23,
24 },
25 end: Position {
26 line: 41,
27 character: 28,
28 },
29 },
30 severity: Some(
31 Warning,
32 ),
33 code: Some(
34 String(
35 "trivially_copy_pass_by_ref",
36 ),
37 ),
38 source: Some(
39 "clippy",
40 ),
41 message: "this argument is passed by reference, but would be more efficient if passed by value\n#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]\nfor further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
42 related_information: Some(
43 [
44 DiagnosticRelatedInformation {
45 location: Location {
46 uri: "file:///test/compiler/lib.rs",
47 range: Range {
48 start: Position {
49 line: 0,
50 character: 8,
51 },
52 end: Position {
53 line: 0,
54 character: 19,
55 },
56 },
57 },
58 message: "lint level defined here",
59 },
60 ],
61 ),
62 tags: None,
63 },
64 suggested_fixes: [
65 SuggestedFix {
66 title: "consider passing by value instead: \'self\'",
67 location: Location {
68 uri: "file:///test/compiler/mir/tagset.rs",
69 range: Range {
70 start: Position {
71 line: 41,
72 character: 23,
73 },
74 end: Position {
75 line: 41,
76 character: 28,
77 },
78 },
79 },
80 replacement: "self",
81 applicability: Unspecified,
82 diagnostics: [],
83 },
84 ],
85}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_handles_macro_location.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_handles_macro_location.snap
new file mode 100644
index 000000000..19510ecc1
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_handles_macro_location.snap
@@ -0,0 +1,46 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/src/main.rs",
8 range: Range {
9 start: Position {
10 line: 1,
11 character: 4,
12 },
13 end: Position {
14 line: 1,
15 character: 26,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 1,
23 character: 4,
24 },
25 end: Position {
26 line: 1,
27 character: 26,
28 },
29 },
30 severity: Some(
31 Error,
32 ),
33 code: Some(
34 String(
35 "E0277",
36 ),
37 ),
38 source: Some(
39 "rustc",
40 ),
41 message: "can\'t compare `{integer}` with `&str`\nthe trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
42 related_information: None,
43 tags: None,
44 },
45 suggested_fixes: [],
46}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_macro_compiler_error.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_macro_compiler_error.snap
new file mode 100644
index 000000000..92f7eec05
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_macro_compiler_error.snap
@@ -0,0 +1,61 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/crates/ra_hir_def/src/data.rs",
8 range: Range {
9 start: Position {
10 line: 79,
11 character: 15,
12 },
13 end: Position {
14 line: 79,
15 character: 41,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 79,
23 character: 15,
24 },
25 end: Position {
26 line: 79,
27 character: 41,
28 },
29 },
30 severity: Some(
31 Error,
32 ),
33 code: None,
34 source: Some(
35 "rustc",
36 ),
37 message: "Please register your known path in the path module",
38 related_information: Some(
39 [
40 DiagnosticRelatedInformation {
41 location: Location {
42 uri: "file:///test/crates/ra_hir_def/src/path.rs",
43 range: Range {
44 start: Position {
45 line: 264,
46 character: 8,
47 },
48 end: Position {
49 line: 264,
50 character: 76,
51 },
52 },
53 },
54 message: "Error originated from macro here",
55 },
56 ],
57 ),
58 tags: None,
59 },
60 suggested_fixes: [],
61}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_incompatible_type_for_trait.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_incompatible_type_for_trait.snap
new file mode 100644
index 000000000..cf683e4b6
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_incompatible_type_for_trait.snap
@@ -0,0 +1,46 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/compiler/ty/list_iter.rs",
8 range: Range {
9 start: Position {
10 line: 51,
11 character: 4,
12 },
13 end: Position {
14 line: 51,
15 character: 47,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 51,
23 character: 4,
24 },
25 end: Position {
26 line: 51,
27 character: 47,
28 },
29 },
30 severity: Some(
31 Error,
32 ),
33 code: Some(
34 String(
35 "E0053",
36 ),
37 ),
38 source: Some(
39 "rustc",
40 ),
41 message: "method `next` has an incompatible type for trait\nexpected type `fn(&mut ty::list_iter::ListIterator<\'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<\'list, M>) -> std::option::Option<&\'list ty::Ref<M>>`",
42 related_information: None,
43 tags: None,
44 },
45 suggested_fixes: [],
46}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_mismatched_type.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_mismatched_type.snap
new file mode 100644
index 000000000..8c1483c74
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_mismatched_type.snap
@@ -0,0 +1,46 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/runtime/compiler_support.rs",
8 range: Range {
9 start: Position {
10 line: 47,
11 character: 64,
12 },
13 end: Position {
14 line: 47,
15 character: 69,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 47,
23 character: 64,
24 },
25 end: Position {
26 line: 47,
27 character: 69,
28 },
29 },
30 severity: Some(
31 Error,
32 ),
33 code: Some(
34 String(
35 "E0308",
36 ),
37 ),
38 source: Some(
39 "rustc",
40 ),
41 message: "mismatched types\nexpected usize, found u32",
42 related_information: None,
43 tags: None,
44 },
45 suggested_fixes: [],
46}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_unused_variable.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_unused_variable.snap
new file mode 100644
index 000000000..eb5a2247b
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_unused_variable.snap
@@ -0,0 +1,70 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/driver/subcommand/repl.rs",
8 range: Range {
9 start: Position {
10 line: 290,
11 character: 8,
12 },
13 end: Position {
14 line: 290,
15 character: 11,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 290,
23 character: 8,
24 },
25 end: Position {
26 line: 290,
27 character: 11,
28 },
29 },
30 severity: Some(
31 Warning,
32 ),
33 code: Some(
34 String(
35 "unused_variables",
36 ),
37 ),
38 source: Some(
39 "rustc",
40 ),
41 message: "unused variable: `foo`\n#[warn(unused_variables)] on by default",
42 related_information: None,
43 tags: Some(
44 [
45 Unnecessary,
46 ],
47 ),
48 },
49 suggested_fixes: [
50 SuggestedFix {
51 title: "consider prefixing with an underscore: \'_foo\'",
52 location: Location {
53 uri: "file:///test/driver/subcommand/repl.rs",
54 range: Range {
55 start: Position {
56 line: 290,
57 character: 8,
58 },
59 end: Position {
60 line: 290,
61 character: 11,
62 },
63 },
64 },
65 replacement: "_foo",
66 applicability: MachineApplicable,
67 diagnostics: [],
68 },
69 ],
70}
diff --git a/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_wrong_number_of_parameters.snap b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_wrong_number_of_parameters.snap
new file mode 100644
index 000000000..2f4518931
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/snapshots/test__snap_rustc_wrong_number_of_parameters.snap
@@ -0,0 +1,65 @@
1---
2source: crates/ra_cargo_watch/src/conv/test.rs
3expression: diag
4---
5MappedRustDiagnostic {
6 location: Location {
7 uri: "file:///test/compiler/ty/select.rs",
8 range: Range {
9 start: Position {
10 line: 103,
11 character: 17,
12 },
13 end: Position {
14 line: 103,
15 character: 29,
16 },
17 },
18 },
19 diagnostic: Diagnostic {
20 range: Range {
21 start: Position {
22 line: 103,
23 character: 17,
24 },
25 end: Position {
26 line: 103,
27 character: 29,
28 },
29 },
30 severity: Some(
31 Error,
32 ),
33 code: Some(
34 String(
35 "E0061",
36 ),
37 ),
38 source: Some(
39 "rustc",
40 ),
41 message: "this function takes 2 parameters but 3 parameters were supplied\nexpected 2 parameters",
42 related_information: Some(
43 [
44 DiagnosticRelatedInformation {
45 location: Location {
46 uri: "file:///test/compiler/ty/select.rs",
47 range: Range {
48 start: Position {
49 line: 218,
50 character: 4,
51 },
52 end: Position {
53 line: 230,
54 character: 5,
55 },
56 },
57 },
58 message: "defined here",
59 },
60 ],
61 ),
62 tags: None,
63 },
64 suggested_fixes: [],
65}
diff --git a/crates/ra_cargo_watch/src/conv/test.rs b/crates/ra_cargo_watch/src/conv/test.rs
new file mode 100644
index 000000000..381992388
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/test.rs
@@ -0,0 +1,929 @@
1//! This module contains the large and verbose snapshot tests for the
2//! conversions between `cargo check` json and LSP diagnostics.
3use crate::*;
4
5fn parse_diagnostic(val: &str) -> cargo_metadata::diagnostic::Diagnostic {
6 serde_json::from_str::<cargo_metadata::diagnostic::Diagnostic>(val).unwrap()
7}
8
9#[test]
10fn snap_rustc_incompatible_type_for_trait() {
11 let diag = parse_diagnostic(
12 r##"{
13 "message": "method `next` has an incompatible type for trait",
14 "code": {
15 "code": "E0053",
16 "explanation": "\nThe parameters of any trait method must match between a trait implementation\nand the trait definition.\n\nHere are a couple examples of this error:\n\n```compile_fail,E0053\ntrait Foo {\n fn foo(x: u16);\n fn bar(&self);\n}\n\nstruct Bar;\n\nimpl Foo for Bar {\n // error, expected u16, found i16\n fn foo(x: i16) { }\n\n // error, types differ in mutability\n fn bar(&mut self) { }\n}\n```\n"
17 },
18 "level": "error",
19 "spans": [
20 {
21 "file_name": "compiler/ty/list_iter.rs",
22 "byte_start": 1307,
23 "byte_end": 1350,
24 "line_start": 52,
25 "line_end": 52,
26 "column_start": 5,
27 "column_end": 48,
28 "is_primary": true,
29 "text": [
30 {
31 "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {",
32 "highlight_start": 5,
33 "highlight_end": 48
34 }
35 ],
36 "label": "types differ in mutability",
37 "suggested_replacement": null,
38 "suggestion_applicability": null,
39 "expansion": null
40 }
41 ],
42 "children": [
43 {
44 "message": "expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`",
45 "code": null,
46 "level": "note",
47 "spans": [],
48 "children": [],
49 "rendered": null
50 }
51 ],
52 "rendered": "error[E0053]: method `next` has an incompatible type for trait\n --> compiler/ty/list_iter.rs:52:5\n |\n52 | fn next(&self) -> Option<&'list ty::Ref<M>> {\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ types differ in mutability\n |\n = note: expected type `fn(&mut ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&ty::Ref<M>>`\n found type `fn(&ty::list_iter::ListIterator<'list, M>) -> std::option::Option<&'list ty::Ref<M>>`\n\n"
53 }
54 "##,
55 );
56
57 let workspace_root = PathBuf::from("/test/");
58 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
59 insta::assert_debug_snapshot!(diag);
60}
61
62#[test]
63fn snap_rustc_unused_variable() {
64 let diag = parse_diagnostic(
65 r##"{
66"message": "unused variable: `foo`",
67"code": {
68 "code": "unused_variables",
69 "explanation": null
70},
71"level": "warning",
72"spans": [
73 {
74 "file_name": "driver/subcommand/repl.rs",
75 "byte_start": 9228,
76 "byte_end": 9231,
77 "line_start": 291,
78 "line_end": 291,
79 "column_start": 9,
80 "column_end": 12,
81 "is_primary": true,
82 "text": [
83 {
84 "text": " let foo = 42;",
85 "highlight_start": 9,
86 "highlight_end": 12
87 }
88 ],
89 "label": null,
90 "suggested_replacement": null,
91 "suggestion_applicability": null,
92 "expansion": null
93 }
94],
95"children": [
96 {
97 "message": "#[warn(unused_variables)] on by default",
98 "code": null,
99 "level": "note",
100 "spans": [],
101 "children": [],
102 "rendered": null
103 },
104 {
105 "message": "consider prefixing with an underscore",
106 "code": null,
107 "level": "help",
108 "spans": [
109 {
110 "file_name": "driver/subcommand/repl.rs",
111 "byte_start": 9228,
112 "byte_end": 9231,
113 "line_start": 291,
114 "line_end": 291,
115 "column_start": 9,
116 "column_end": 12,
117 "is_primary": true,
118 "text": [
119 {
120 "text": " let foo = 42;",
121 "highlight_start": 9,
122 "highlight_end": 12
123 }
124 ],
125 "label": null,
126 "suggested_replacement": "_foo",
127 "suggestion_applicability": "MachineApplicable",
128 "expansion": null
129 }
130 ],
131 "children": [],
132 "rendered": null
133 }
134],
135"rendered": "warning: unused variable: `foo`\n --> driver/subcommand/repl.rs:291:9\n |\n291 | let foo = 42;\n | ^^^ help: consider prefixing with an underscore: `_foo`\n |\n = note: #[warn(unused_variables)] on by default\n\n"
136}"##,
137 );
138
139 let workspace_root = PathBuf::from("/test/");
140 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
141 insta::assert_debug_snapshot!(diag);
142}
143
144#[test]
145fn snap_rustc_wrong_number_of_parameters() {
146 let diag = parse_diagnostic(
147 r##"{
148"message": "this function takes 2 parameters but 3 parameters were supplied",
149"code": {
150 "code": "E0061",
151 "explanation": "\nThe number of arguments passed to a function must match the number of arguments\nspecified in the function signature.\n\nFor example, a function like:\n\n```\nfn f(a: u16, b: &str) {}\n```\n\nMust always be called with exactly two arguments, e.g., `f(2, \"test\")`.\n\nNote that Rust does not have a notion of optional function arguments or\nvariadic functions (except for its C-FFI).\n"
152},
153"level": "error",
154"spans": [
155 {
156 "file_name": "compiler/ty/select.rs",
157 "byte_start": 8787,
158 "byte_end": 9241,
159 "line_start": 219,
160 "line_end": 231,
161 "column_start": 5,
162 "column_end": 6,
163 "is_primary": false,
164 "text": [
165 {
166 "text": " pub fn add_evidence(",
167 "highlight_start": 5,
168 "highlight_end": 25
169 },
170 {
171 "text": " &mut self,",
172 "highlight_start": 1,
173 "highlight_end": 19
174 },
175 {
176 "text": " target_poly: &ty::Ref<ty::Poly>,",
177 "highlight_start": 1,
178 "highlight_end": 41
179 },
180 {
181 "text": " evidence_poly: &ty::Ref<ty::Poly>,",
182 "highlight_start": 1,
183 "highlight_end": 43
184 },
185 {
186 "text": " ) {",
187 "highlight_start": 1,
188 "highlight_end": 8
189 },
190 {
191 "text": " match target_poly {",
192 "highlight_start": 1,
193 "highlight_end": 28
194 },
195 {
196 "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
197 "highlight_start": 1,
198 "highlight_end": 81
199 },
200 {
201 "text": " ty::Ref::Fixed(target_ty) => {",
202 "highlight_start": 1,
203 "highlight_end": 43
204 },
205 {
206 "text": " let evidence_ty = evidence_poly.resolve_to_ty();",
207 "highlight_start": 1,
208 "highlight_end": 65
209 },
210 {
211 "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
212 "highlight_start": 1,
213 "highlight_end": 76
214 },
215 {
216 "text": " }",
217 "highlight_start": 1,
218 "highlight_end": 14
219 },
220 {
221 "text": " }",
222 "highlight_start": 1,
223 "highlight_end": 10
224 },
225 {
226 "text": " }",
227 "highlight_start": 1,
228 "highlight_end": 6
229 }
230 ],
231 "label": "defined here",
232 "suggested_replacement": null,
233 "suggestion_applicability": null,
234 "expansion": null
235 },
236 {
237 "file_name": "compiler/ty/select.rs",
238 "byte_start": 4045,
239 "byte_end": 4057,
240 "line_start": 104,
241 "line_end": 104,
242 "column_start": 18,
243 "column_end": 30,
244 "is_primary": true,
245 "text": [
246 {
247 "text": " self.add_evidence(target_fixed, evidence_fixed, false);",
248 "highlight_start": 18,
249 "highlight_end": 30
250 }
251 ],
252 "label": "expected 2 parameters",
253 "suggested_replacement": null,
254 "suggestion_applicability": null,
255 "expansion": null
256 }
257],
258"children": [],
259"rendered": "error[E0061]: this function takes 2 parameters but 3 parameters were supplied\n --> compiler/ty/select.rs:104:18\n |\n104 | self.add_evidence(target_fixed, evidence_fixed, false);\n | ^^^^^^^^^^^^ expected 2 parameters\n...\n219 | / pub fn add_evidence(\n220 | | &mut self,\n221 | | target_poly: &ty::Ref<ty::Poly>,\n222 | | evidence_poly: &ty::Ref<ty::Poly>,\n... |\n230 | | }\n231 | | }\n | |_____- defined here\n\n"
260}"##,
261 );
262
263 let workspace_root = PathBuf::from("/test/");
264 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
265 insta::assert_debug_snapshot!(diag);
266}
267
268#[test]
269fn snap_clippy_pass_by_ref() {
270 let diag = parse_diagnostic(
271 r##"{
272"message": "this argument is passed by reference, but would be more efficient if passed by value",
273"code": {
274 "code": "clippy::trivially_copy_pass_by_ref",
275 "explanation": null
276},
277"level": "warning",
278"spans": [
279 {
280 "file_name": "compiler/mir/tagset.rs",
281 "byte_start": 941,
282 "byte_end": 946,
283 "line_start": 42,
284 "line_end": 42,
285 "column_start": 24,
286 "column_end": 29,
287 "is_primary": true,
288 "text": [
289 {
290 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
291 "highlight_start": 24,
292 "highlight_end": 29
293 }
294 ],
295 "label": null,
296 "suggested_replacement": null,
297 "suggestion_applicability": null,
298 "expansion": null
299 }
300],
301"children": [
302 {
303 "message": "lint level defined here",
304 "code": null,
305 "level": "note",
306 "spans": [
307 {
308 "file_name": "compiler/lib.rs",
309 "byte_start": 8,
310 "byte_end": 19,
311 "line_start": 1,
312 "line_end": 1,
313 "column_start": 9,
314 "column_end": 20,
315 "is_primary": true,
316 "text": [
317 {
318 "text": "#![warn(clippy::all)]",
319 "highlight_start": 9,
320 "highlight_end": 20
321 }
322 ],
323 "label": null,
324 "suggested_replacement": null,
325 "suggestion_applicability": null,
326 "expansion": null
327 }
328 ],
329 "children": [],
330 "rendered": null
331 },
332 {
333 "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
334 "code": null,
335 "level": "note",
336 "spans": [],
337 "children": [],
338 "rendered": null
339 },
340 {
341 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
342 "code": null,
343 "level": "help",
344 "spans": [],
345 "children": [],
346 "rendered": null
347 },
348 {
349 "message": "consider passing by value instead",
350 "code": null,
351 "level": "help",
352 "spans": [
353 {
354 "file_name": "compiler/mir/tagset.rs",
355 "byte_start": 941,
356 "byte_end": 946,
357 "line_start": 42,
358 "line_end": 42,
359 "column_start": 24,
360 "column_end": 29,
361 "is_primary": true,
362 "text": [
363 {
364 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
365 "highlight_start": 24,
366 "highlight_end": 29
367 }
368 ],
369 "label": null,
370 "suggested_replacement": "self",
371 "suggestion_applicability": "Unspecified",
372 "expansion": null
373 }
374 ],
375 "children": [],
376 "rendered": null
377 }
378],
379"rendered": "warning: this argument is passed by reference, but would be more efficient if passed by value\n --> compiler/mir/tagset.rs:42:24\n |\n42 | pub fn is_disjoint(&self, other: Self) -> bool {\n | ^^^^^ help: consider passing by value instead: `self`\n |\nnote: lint level defined here\n --> compiler/lib.rs:1:9\n |\n1 | #![warn(clippy::all)]\n | ^^^^^^^^^^^\n = note: #[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref\n\n"
380}"##,
381 );
382
383 let workspace_root = PathBuf::from("/test/");
384 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
385 insta::assert_debug_snapshot!(diag);
386}
387
388#[test]
389fn snap_rustc_mismatched_type() {
390 let diag = parse_diagnostic(
391 r##"{
392"message": "mismatched types",
393"code": {
394 "code": "E0308",
395 "explanation": "\nThis error occurs when the compiler was unable to infer the concrete type of a\nvariable. It can occur for several cases, the most common of which is a\nmismatch in the expected type that the compiler inferred for a variable's\ninitializing expression, and the actual type explicitly assigned to the\nvariable.\n\nFor example:\n\n```compile_fail,E0308\nlet x: i32 = \"I am not a number!\";\n// ~~~ ~~~~~~~~~~~~~~~~~~~~\n// | |\n// | initializing expression;\n// | compiler infers type `&str`\n// |\n// type `i32` assigned to variable `x`\n```\n"
396},
397"level": "error",
398"spans": [
399 {
400 "file_name": "runtime/compiler_support.rs",
401 "byte_start": 1589,
402 "byte_end": 1594,
403 "line_start": 48,
404 "line_end": 48,
405 "column_start": 65,
406 "column_end": 70,
407 "is_primary": true,
408 "text": [
409 {
410 "text": " let layout = alloc::Layout::from_size_align_unchecked(size, align);",
411 "highlight_start": 65,
412 "highlight_end": 70
413 }
414 ],
415 "label": "expected usize, found u32",
416 "suggested_replacement": null,
417 "suggestion_applicability": null,
418 "expansion": null
419 }
420],
421"children": [],
422"rendered": "error[E0308]: mismatched types\n --> runtime/compiler_support.rs:48:65\n |\n48 | let layout = alloc::Layout::from_size_align_unchecked(size, align);\n | ^^^^^ expected usize, found u32\n\n"
423}"##,
424 );
425
426 let workspace_root = PathBuf::from("/test/");
427 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
428 insta::assert_debug_snapshot!(diag);
429}
430
431#[test]
432fn snap_handles_macro_location() {
433 let diag = parse_diagnostic(
434 r##"{
435"rendered": "error[E0277]: can't compare `{integer}` with `&str`\n --> src/main.rs:2:5\n |\n2 | assert_eq!(1, \"love\");\n | ^^^^^^^^^^^^^^^^^^^^^^ no implementation for `{integer} == &str`\n |\n = help: the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`\n = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)\n\n",
436"children": [
437 {
438 "children": [],
439 "code": null,
440 "level": "help",
441 "message": "the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
442 "rendered": null,
443 "spans": []
444 }
445],
446"code": {
447 "code": "E0277",
448 "explanation": "\nYou tried to use a type which doesn't implement some trait in a place which\nexpected that trait. Erroneous code example:\n\n```compile_fail,E0277\n// here we declare the Foo trait with a bar method\ntrait Foo {\n fn bar(&self);\n}\n\n// we now declare a function which takes an object implementing the Foo trait\nfn some_func<T: Foo>(foo: T) {\n foo.bar();\n}\n\nfn main() {\n // we now call the method with the i32 type, which doesn't implement\n // the Foo trait\n some_func(5i32); // error: the trait bound `i32 : Foo` is not satisfied\n}\n```\n\nIn order to fix this error, verify that the type you're using does implement\nthe trait. Example:\n\n```\ntrait Foo {\n fn bar(&self);\n}\n\nfn some_func<T: Foo>(foo: T) {\n foo.bar(); // we can now use this method since i32 implements the\n // Foo trait\n}\n\n// we implement the trait on the i32 type\nimpl Foo for i32 {\n fn bar(&self) {}\n}\n\nfn main() {\n some_func(5i32); // ok!\n}\n```\n\nOr in a generic context, an erroneous code example would look like:\n\n```compile_fail,E0277\nfn some_func<T>(foo: T) {\n println!(\"{:?}\", foo); // error: the trait `core::fmt::Debug` is not\n // implemented for the type `T`\n}\n\nfn main() {\n // We now call the method with the i32 type,\n // which *does* implement the Debug trait.\n some_func(5i32);\n}\n```\n\nNote that the error here is in the definition of the generic function: Although\nwe only call it with a parameter that does implement `Debug`, the compiler\nstill rejects the function: It must work with all possible input types. In\norder to make this example compile, we need to restrict the generic type we're\naccepting:\n\n```\nuse std::fmt;\n\n// Restrict the input type to types that implement Debug.\nfn some_func<T: fmt::Debug>(foo: T) {\n println!(\"{:?}\", foo);\n}\n\nfn main() {\n // Calling the method is still fine, as i32 implements Debug.\n some_func(5i32);\n\n // This would fail to compile now:\n // struct WithoutDebug;\n // some_func(WithoutDebug);\n}\n```\n\nRust only looks at the signature of the called function, as such it must\nalready specify all requirements that will be used for every type parameter.\n"
449},
450"level": "error",
451"message": "can't compare `{integer}` with `&str`",
452"spans": [
453 {
454 "byte_end": 155,
455 "byte_start": 153,
456 "column_end": 33,
457 "column_start": 31,
458 "expansion": {
459 "def_site_span": {
460 "byte_end": 940,
461 "byte_start": 0,
462 "column_end": 6,
463 "column_start": 1,
464 "expansion": null,
465 "file_name": "<::core::macros::assert_eq macros>",
466 "is_primary": false,
467 "label": null,
468 "line_end": 36,
469 "line_start": 1,
470 "suggested_replacement": null,
471 "suggestion_applicability": null,
472 "text": [
473 {
474 "highlight_end": 35,
475 "highlight_start": 1,
476 "text": "($ left : expr, $ right : expr) =>"
477 },
478 {
479 "highlight_end": 3,
480 "highlight_start": 1,
481 "text": "({"
482 },
483 {
484 "highlight_end": 33,
485 "highlight_start": 1,
486 "text": " match (& $ left, & $ right)"
487 },
488 {
489 "highlight_end": 7,
490 "highlight_start": 1,
491 "text": " {"
492 },
493 {
494 "highlight_end": 34,
495 "highlight_start": 1,
496 "text": " (left_val, right_val) =>"
497 },
498 {
499 "highlight_end": 11,
500 "highlight_start": 1,
501 "text": " {"
502 },
503 {
504 "highlight_end": 46,
505 "highlight_start": 1,
506 "text": " if ! (* left_val == * right_val)"
507 },
508 {
509 "highlight_end": 15,
510 "highlight_start": 1,
511 "text": " {"
512 },
513 {
514 "highlight_end": 25,
515 "highlight_start": 1,
516 "text": " panic !"
517 },
518 {
519 "highlight_end": 57,
520 "highlight_start": 1,
521 "text": " (r#\"assertion failed: `(left == right)`"
522 },
523 {
524 "highlight_end": 16,
525 "highlight_start": 1,
526 "text": " left: `{:?}`,"
527 },
528 {
529 "highlight_end": 18,
530 "highlight_start": 1,
531 "text": " right: `{:?}`\"#,"
532 },
533 {
534 "highlight_end": 47,
535 "highlight_start": 1,
536 "text": " & * left_val, & * right_val)"
537 },
538 {
539 "highlight_end": 15,
540 "highlight_start": 1,
541 "text": " }"
542 },
543 {
544 "highlight_end": 11,
545 "highlight_start": 1,
546 "text": " }"
547 },
548 {
549 "highlight_end": 7,
550 "highlight_start": 1,
551 "text": " }"
552 },
553 {
554 "highlight_end": 42,
555 "highlight_start": 1,
556 "text": " }) ; ($ left : expr, $ right : expr,) =>"
557 },
558 {
559 "highlight_end": 49,
560 "highlight_start": 1,
561 "text": "({ $ crate :: assert_eq ! ($ left, $ right) }) ;"
562 },
563 {
564 "highlight_end": 53,
565 "highlight_start": 1,
566 "text": "($ left : expr, $ right : expr, $ ($ arg : tt) +) =>"
567 },
568 {
569 "highlight_end": 3,
570 "highlight_start": 1,
571 "text": "({"
572 },
573 {
574 "highlight_end": 37,
575 "highlight_start": 1,
576 "text": " match (& ($ left), & ($ right))"
577 },
578 {
579 "highlight_end": 7,
580 "highlight_start": 1,
581 "text": " {"
582 },
583 {
584 "highlight_end": 34,
585 "highlight_start": 1,
586 "text": " (left_val, right_val) =>"
587 },
588 {
589 "highlight_end": 11,
590 "highlight_start": 1,
591 "text": " {"
592 },
593 {
594 "highlight_end": 46,
595 "highlight_start": 1,
596 "text": " if ! (* left_val == * right_val)"
597 },
598 {
599 "highlight_end": 15,
600 "highlight_start": 1,
601 "text": " {"
602 },
603 {
604 "highlight_end": 25,
605 "highlight_start": 1,
606 "text": " panic !"
607 },
608 {
609 "highlight_end": 57,
610 "highlight_start": 1,
611 "text": " (r#\"assertion failed: `(left == right)`"
612 },
613 {
614 "highlight_end": 16,
615 "highlight_start": 1,
616 "text": " left: `{:?}`,"
617 },
618 {
619 "highlight_end": 22,
620 "highlight_start": 1,
621 "text": " right: `{:?}`: {}\"#,"
622 },
623 {
624 "highlight_end": 72,
625 "highlight_start": 1,
626 "text": " & * left_val, & * right_val, $ crate :: format_args !"
627 },
628 {
629 "highlight_end": 33,
630 "highlight_start": 1,
631 "text": " ($ ($ arg) +))"
632 },
633 {
634 "highlight_end": 15,
635 "highlight_start": 1,
636 "text": " }"
637 },
638 {
639 "highlight_end": 11,
640 "highlight_start": 1,
641 "text": " }"
642 },
643 {
644 "highlight_end": 7,
645 "highlight_start": 1,
646 "text": " }"
647 },
648 {
649 "highlight_end": 6,
650 "highlight_start": 1,
651 "text": " }) ;"
652 }
653 ]
654 },
655 "macro_decl_name": "assert_eq!",
656 "span": {
657 "byte_end": 38,
658 "byte_start": 16,
659 "column_end": 27,
660 "column_start": 5,
661 "expansion": null,
662 "file_name": "src/main.rs",
663 "is_primary": false,
664 "label": null,
665 "line_end": 2,
666 "line_start": 2,
667 "suggested_replacement": null,
668 "suggestion_applicability": null,
669 "text": [
670 {
671 "highlight_end": 27,
672 "highlight_start": 5,
673 "text": " assert_eq!(1, \"love\");"
674 }
675 ]
676 }
677 },
678 "file_name": "<::core::macros::assert_eq macros>",
679 "is_primary": true,
680 "label": "no implementation for `{integer} == &str`",
681 "line_end": 7,
682 "line_start": 7,
683 "suggested_replacement": null,
684 "suggestion_applicability": null,
685 "text": [
686 {
687 "highlight_end": 33,
688 "highlight_start": 31,
689 "text": " if ! (* left_val == * right_val)"
690 }
691 ]
692 }
693]
694}"##,
695 );
696
697 let workspace_root = PathBuf::from("/test/");
698 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
699 insta::assert_debug_snapshot!(diag);
700}
701
702#[test]
703fn snap_macro_compiler_error() {
704 let diag = parse_diagnostic(
705 r##"{
706 "rendered": "error: Please register your known path in the path module\n --> crates/ra_hir_def/src/path.rs:265:9\n |\n265 | compile_error!(\"Please register your known path in the path module\")\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n | \n ::: crates/ra_hir_def/src/data.rs:80:16\n |\n80 | let path = path![std::future::Future];\n | -------------------------- in this macro invocation\n\n",
707 "children": [],
708 "code": null,
709 "level": "error",
710 "message": "Please register your known path in the path module",
711 "spans": [
712 {
713 "byte_end": 8285,
714 "byte_start": 8217,
715 "column_end": 77,
716 "column_start": 9,
717 "expansion": {
718 "def_site_span": {
719 "byte_end": 8294,
720 "byte_start": 7858,
721 "column_end": 2,
722 "column_start": 1,
723 "expansion": null,
724 "file_name": "crates/ra_hir_def/src/path.rs",
725 "is_primary": false,
726 "label": null,
727 "line_end": 267,
728 "line_start": 254,
729 "suggested_replacement": null,
730 "suggestion_applicability": null,
731 "text": [
732 {
733 "highlight_end": 28,
734 "highlight_start": 1,
735 "text": "macro_rules! __known_path {"
736 },
737 {
738 "highlight_end": 37,
739 "highlight_start": 1,
740 "text": " (std::iter::IntoIterator) => {};"
741 },
742 {
743 "highlight_end": 33,
744 "highlight_start": 1,
745 "text": " (std::result::Result) => {};"
746 },
747 {
748 "highlight_end": 29,
749 "highlight_start": 1,
750 "text": " (std::ops::Range) => {};"
751 },
752 {
753 "highlight_end": 33,
754 "highlight_start": 1,
755 "text": " (std::ops::RangeFrom) => {};"
756 },
757 {
758 "highlight_end": 33,
759 "highlight_start": 1,
760 "text": " (std::ops::RangeFull) => {};"
761 },
762 {
763 "highlight_end": 31,
764 "highlight_start": 1,
765 "text": " (std::ops::RangeTo) => {};"
766 },
767 {
768 "highlight_end": 40,
769 "highlight_start": 1,
770 "text": " (std::ops::RangeToInclusive) => {};"
771 },
772 {
773 "highlight_end": 38,
774 "highlight_start": 1,
775 "text": " (std::ops::RangeInclusive) => {};"
776 },
777 {
778 "highlight_end": 27,
779 "highlight_start": 1,
780 "text": " (std::ops::Try) => {};"
781 },
782 {
783 "highlight_end": 22,
784 "highlight_start": 1,
785 "text": " ($path:path) => {"
786 },
787 {
788 "highlight_end": 77,
789 "highlight_start": 1,
790 "text": " compile_error!(\"Please register your known path in the path module\")"
791 },
792 {
793 "highlight_end": 7,
794 "highlight_start": 1,
795 "text": " };"
796 },
797 {
798 "highlight_end": 2,
799 "highlight_start": 1,
800 "text": "}"
801 }
802 ]
803 },
804 "macro_decl_name": "$crate::__known_path!",
805 "span": {
806 "byte_end": 8427,
807 "byte_start": 8385,
808 "column_end": 51,
809 "column_start": 9,
810 "expansion": {
811 "def_site_span": {
812 "byte_end": 8611,
813 "byte_start": 8312,
814 "column_end": 2,
815 "column_start": 1,
816 "expansion": null,
817 "file_name": "crates/ra_hir_def/src/path.rs",
818 "is_primary": false,
819 "label": null,
820 "line_end": 277,
821 "line_start": 270,
822 "suggested_replacement": null,
823 "suggestion_applicability": null,
824 "text": [
825 {
826 "highlight_end": 22,
827 "highlight_start": 1,
828 "text": "macro_rules! __path {"
829 },
830 {
831 "highlight_end": 43,
832 "highlight_start": 1,
833 "text": " ($start:ident $(:: $seg:ident)*) => ({"
834 },
835 {
836 "highlight_end": 51,
837 "highlight_start": 1,
838 "text": " $crate::__known_path!($start $(:: $seg)*);"
839 },
840 {
841 "highlight_end": 87,
842 "highlight_start": 1,
843 "text": " $crate::path::ModPath::from_simple_segments($crate::path::PathKind::Abs, vec!["
844 },
845 {
846 "highlight_end": 76,
847 "highlight_start": 1,
848 "text": " $crate::path::__name![$start], $($crate::path::__name![$seg],)*"
849 },
850 {
851 "highlight_end": 11,
852 "highlight_start": 1,
853 "text": " ])"
854 },
855 {
856 "highlight_end": 8,
857 "highlight_start": 1,
858 "text": " });"
859 },
860 {
861 "highlight_end": 2,
862 "highlight_start": 1,
863 "text": "}"
864 }
865 ]
866 },
867 "macro_decl_name": "path!",
868 "span": {
869 "byte_end": 2966,
870 "byte_start": 2940,
871 "column_end": 42,
872 "column_start": 16,
873 "expansion": null,
874 "file_name": "crates/ra_hir_def/src/data.rs",
875 "is_primary": false,
876 "label": null,
877 "line_end": 80,
878 "line_start": 80,
879 "suggested_replacement": null,
880 "suggestion_applicability": null,
881 "text": [
882 {
883 "highlight_end": 42,
884 "highlight_start": 16,
885 "text": " let path = path![std::future::Future];"
886 }
887 ]
888 }
889 },
890 "file_name": "crates/ra_hir_def/src/path.rs",
891 "is_primary": false,
892 "label": null,
893 "line_end": 272,
894 "line_start": 272,
895 "suggested_replacement": null,
896 "suggestion_applicability": null,
897 "text": [
898 {
899 "highlight_end": 51,
900 "highlight_start": 9,
901 "text": " $crate::__known_path!($start $(:: $seg)*);"
902 }
903 ]
904 }
905 },
906 "file_name": "crates/ra_hir_def/src/path.rs",
907 "is_primary": true,
908 "label": null,
909 "line_end": 265,
910 "line_start": 265,
911 "suggested_replacement": null,
912 "suggestion_applicability": null,
913 "text": [
914 {
915 "highlight_end": 77,
916 "highlight_start": 9,
917 "text": " compile_error!(\"Please register your known path in the path module\")"
918 }
919 ]
920 }
921 ]
922}
923 "##,
924 );
925
926 let workspace_root = PathBuf::from("/test/");
927 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
928 insta::assert_debug_snapshot!(diag);
929}
diff --git a/crates/ra_cargo_watch/src/lib.rs b/crates/ra_cargo_watch/src/lib.rs
new file mode 100644
index 000000000..9bc0fd405
--- /dev/null
+++ b/crates/ra_cargo_watch/src/lib.rs
@@ -0,0 +1,394 @@
1//! cargo_check provides the functionality needed to run `cargo check` or
2//! another compatible command (f.x. clippy) in a background thread and provide
3//! LSP diagnostics based on the output of the command.
4use cargo_metadata::Message;
5use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender};
6use lsp_types::{
7 Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin, WorkDoneProgressEnd,
8 WorkDoneProgressReport,
9};
10use parking_lot::RwLock;
11use std::{
12 collections::HashMap,
13 path::PathBuf,
14 process::{Command, Stdio},
15 sync::Arc,
16 thread::JoinHandle,
17 time::Instant,
18};
19
20mod conv;
21
22use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic, SuggestedFix};
23
24pub use crate::conv::url_from_path_with_drive_lowercasing;
25
26#[derive(Clone, Debug)]
27pub struct CheckOptions {
28 pub enable: bool,
29 pub args: Vec<String>,
30 pub command: String,
31 pub all_targets: bool,
32}
33
34/// CheckWatcher wraps the shared state and communication machinery used for
35/// running `cargo check` (or other compatible command) and providing
36/// diagnostics based on the output.
37/// The spawned thread is shut down when this struct is dropped.
38#[derive(Debug)]
39pub struct CheckWatcher {
40 pub task_recv: Receiver<CheckTask>,
41 pub shared: Arc<RwLock<CheckWatcherSharedState>>,
42 cmd_send: Option<Sender<CheckCommand>>,
43 handle: Option<JoinHandle<()>>,
44}
45
46impl CheckWatcher {
47 pub fn new(options: &CheckOptions, workspace_root: PathBuf) -> CheckWatcher {
48 let options = options.clone();
49 let shared = Arc::new(RwLock::new(CheckWatcherSharedState::new()));
50
51 let (task_send, task_recv) = unbounded::<CheckTask>();
52 let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
53 let shared_ = shared.clone();
54 let handle = std::thread::spawn(move || {
55 let mut check = CheckWatcherState::new(options, workspace_root, shared_);
56 check.run(&task_send, &cmd_recv);
57 });
58 CheckWatcher { task_recv, cmd_send: Some(cmd_send), handle: Some(handle), shared }
59 }
60
61 /// Schedule a re-start of the cargo check worker.
62 pub fn update(&self) {
63 if let Some(cmd_send) = &self.cmd_send {
64 cmd_send.send(CheckCommand::Update).unwrap();
65 }
66 }
67}
68
69impl std::ops::Drop for CheckWatcher {
70 fn drop(&mut self) {
71 if let Some(handle) = self.handle.take() {
72 // Take the sender out of the option
73 let recv = self.cmd_send.take();
74
75 // Dropping the sender finishes the thread loop
76 drop(recv);
77
78 // Join the thread, it should finish shortly. We don't really care
79 // whether it panicked, so it is safe to ignore the result
80 let _ = handle.join();
81 }
82 }
83}
84
85#[derive(Debug)]
86pub struct CheckWatcherSharedState {
87 diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
88 suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
89}
90
91impl CheckWatcherSharedState {
92 fn new() -> CheckWatcherSharedState {
93 CheckWatcherSharedState {
94 diagnostic_collection: HashMap::new(),
95 suggested_fix_collection: HashMap::new(),
96 }
97 }
98
99 /// Clear the cached diagnostics, and schedule updating diagnostics by the
100 /// server, to clear stale results.
101 pub fn clear(&mut self, task_send: &Sender<CheckTask>) {
102 let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect();
103
104 self.diagnostic_collection.clear();
105 self.suggested_fix_collection.clear();
106
107 for uri in cleared_files {
108 task_send.send(CheckTask::Update(uri.clone())).unwrap();
109 }
110 }
111
112 pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> {
113 self.diagnostic_collection.get(uri).map(|d| d.as_slice())
114 }
115
116 pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> {
117 self.suggested_fix_collection.get(uri).map(|d| d.as_slice())
118 }
119
120 fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) {
121 let diagnostics = self.diagnostic_collection.entry(file_uri).or_default();
122
123 // If we're building multiple targets it's possible we've already seen this diagnostic
124 let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic));
125 if is_duplicate {
126 return;
127 }
128
129 diagnostics.push(diagnostic);
130 }
131
132 fn add_suggested_fix_for_diagnostic(
133 &mut self,
134 mut suggested_fix: SuggestedFix,
135 diagnostic: &Diagnostic,
136 ) {
137 let file_uri = suggested_fix.location.uri.clone();
138 let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default();
139
140 let existing_suggestion: Option<&mut SuggestedFix> =
141 file_suggestions.iter_mut().find(|s| s == &&suggested_fix);
142 if let Some(existing_suggestion) = existing_suggestion {
143 // The existing suggestion also applies to this new diagnostic
144 existing_suggestion.diagnostics.push(diagnostic.clone());
145 } else {
146 // We haven't seen this suggestion before
147 suggested_fix.diagnostics.push(diagnostic.clone());
148 file_suggestions.push(suggested_fix);
149 }
150 }
151}
152
153#[derive(Debug)]
154pub enum CheckTask {
155 /// Request a update of the given files diagnostics
156 Update(Url),
157
158 /// Request check progress notification to client
159 Status(WorkDoneProgress),
160}
161
162pub enum CheckCommand {
163 /// Request re-start of check thread
164 Update,
165}
166
167struct CheckWatcherState {
168 options: CheckOptions,
169 workspace_root: PathBuf,
170 watcher: WatchThread,
171 last_update_req: Option<Instant>,
172 shared: Arc<RwLock<CheckWatcherSharedState>>,
173}
174
175impl CheckWatcherState {
176 fn new(
177 options: CheckOptions,
178 workspace_root: PathBuf,
179 shared: Arc<RwLock<CheckWatcherSharedState>>,
180 ) -> CheckWatcherState {
181 let watcher = WatchThread::new(&options, &workspace_root);
182 CheckWatcherState { options, workspace_root, watcher, last_update_req: None, shared }
183 }
184
185 fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
186 loop {
187 select! {
188 recv(&cmd_recv) -> cmd => match cmd {
189 Ok(cmd) => self.handle_command(cmd),
190 Err(RecvError) => {
191 // Command channel has closed, so shut down
192 break;
193 },
194 },
195 recv(self.watcher.message_recv) -> msg => match msg {
196 Ok(msg) => self.handle_message(msg, task_send),
197 Err(RecvError) => {
198 // Watcher finished, replace it with a never channel to
199 // avoid busy-waiting.
200 std::mem::replace(&mut self.watcher.message_recv, never());
201 },
202 }
203 };
204
205 if self.should_recheck() {
206 self.last_update_req.take();
207 self.shared.write().clear(task_send);
208
209 // By replacing the watcher, we drop the previous one which
210 // causes it to shut down automatically.
211 self.watcher = WatchThread::new(&self.options, &self.workspace_root);
212 }
213 }
214 }
215
216 fn should_recheck(&mut self) -> bool {
217 if let Some(_last_update_req) = &self.last_update_req {
218 // We currently only request an update on save, as we need up to
219 // date source on disk for cargo check to do it's magic, so we
220 // don't really need to debounce the requests at this point.
221 return true;
222 }
223 false
224 }
225
226 fn handle_command(&mut self, cmd: CheckCommand) {
227 match cmd {
228 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
229 }
230 }
231
232 fn handle_message(&mut self, msg: CheckEvent, task_send: &Sender<CheckTask>) {
233 match msg {
234 CheckEvent::Begin => {
235 task_send
236 .send(CheckTask::Status(WorkDoneProgress::Begin(WorkDoneProgressBegin {
237 title: "Running 'cargo check'".to_string(),
238 cancellable: Some(false),
239 message: None,
240 percentage: None,
241 })))
242 .unwrap();
243 }
244
245 CheckEvent::End => {
246 task_send
247 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd {
248 message: None,
249 })))
250 .unwrap();
251 }
252
253 CheckEvent::Msg(Message::CompilerArtifact(msg)) => {
254 task_send
255 .send(CheckTask::Status(WorkDoneProgress::Report(WorkDoneProgressReport {
256 cancellable: Some(false),
257 message: Some(msg.target.name),
258 percentage: None,
259 })))
260 .unwrap();
261 }
262
263 CheckEvent::Msg(Message::CompilerMessage(msg)) => {
264 let map_result =
265 match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) {
266 Some(map_result) => map_result,
267 None => return,
268 };
269
270 let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result;
271 let file_uri = location.uri.clone();
272
273 if !suggested_fixes.is_empty() {
274 for suggested_fix in suggested_fixes {
275 self.shared
276 .write()
277 .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic);
278 }
279 }
280 self.shared.write().add_diagnostic(file_uri, diagnostic);
281
282 task_send.send(CheckTask::Update(location.uri)).unwrap();
283 }
284
285 CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
286 CheckEvent::Msg(Message::Unknown) => {}
287 }
288 }
289}
290
291/// WatchThread exists to wrap around the communication needed to be able to
292/// run `cargo check` without blocking. Currently the Rust standard library
293/// doesn't provide a way to read sub-process output without blocking, so we
294/// have to wrap sub-processes output handling in a thread and pass messages
295/// back over a channel.
296/// The correct way to dispose of the thread is to drop it, on which the
297/// sub-process will be killed, and the thread will be joined.
298struct WatchThread {
299 handle: Option<JoinHandle<()>>,
300 message_recv: Receiver<CheckEvent>,
301}
302
303enum CheckEvent {
304 Begin,
305 Msg(cargo_metadata::Message),
306 End,
307}
308
309impl WatchThread {
310 fn new(options: &CheckOptions, workspace_root: &PathBuf) -> WatchThread {
311 let mut args: Vec<String> = vec![
312 options.command.clone(),
313 "--message-format=json".to_string(),
314 "--manifest-path".to_string(),
315 format!("{}/Cargo.toml", workspace_root.to_string_lossy()),
316 ];
317 if options.all_targets {
318 args.push("--all-targets".to_string());
319 }
320 args.extend(options.args.iter().cloned());
321
322 let (message_send, message_recv) = unbounded();
323 let enabled = options.enable;
324 let handle = std::thread::spawn(move || {
325 if !enabled {
326 return;
327 }
328
329 let mut command = Command::new("cargo")
330 .args(&args)
331 .stdout(Stdio::piped())
332 .stderr(Stdio::null())
333 .spawn()
334 .expect("couldn't launch cargo");
335
336 // If we trigger an error here, we will do so in the loop instead,
337 // which will break out of the loop, and continue the shutdown
338 let _ = message_send.send(CheckEvent::Begin);
339
340 for message in cargo_metadata::parse_messages(command.stdout.take().unwrap()) {
341 let message = match message {
342 Ok(message) => message,
343 Err(err) => {
344 log::error!("Invalid json from cargo check, ignoring: {}", err);
345 continue;
346 }
347 };
348
349 match message_send.send(CheckEvent::Msg(message)) {
350 Ok(()) => {}
351 Err(_err) => {
352 // The send channel was closed, so we want to shutdown
353 break;
354 }
355 }
356 }
357
358 // We can ignore any error here, as we are already in the progress
359 // of shutting down.
360 let _ = message_send.send(CheckEvent::End);
361
362 // It is okay to ignore the result, as it only errors if the process is already dead
363 let _ = command.kill();
364
365 // Again, we don't care about the exit status so just ignore the result
366 let _ = command.wait();
367 });
368 WatchThread { handle: Some(handle), message_recv }
369 }
370}
371
372impl std::ops::Drop for WatchThread {
373 fn drop(&mut self) {
374 if let Some(handle) = self.handle.take() {
375 // Replace our reciever with dummy one, so we can drop and close the
376 // one actually communicating with the thread
377 let recv = std::mem::replace(&mut self.message_recv, never());
378
379 // Dropping the original reciever initiates thread sub-process shutdown
380 drop(recv);
381
382 // Join the thread, it should finish shortly. We don't really care
383 // whether it panicked, so it is safe to ignore the result
384 let _ = handle.join();
385 }
386 }
387}
388
389fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
390 left.source == right.source
391 && left.severity == right.severity
392 && left.range == right.range
393 && left.message == right.message
394}