aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_flycheck
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_flycheck')
-rw-r--r--crates/ra_flycheck/Cargo.toml16
-rw-r--r--crates/ra_flycheck/src/conv.rs341
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_clippy_pass_by_ref.snap83
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_handles_macro_location.snap48
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_macro_compiler_error.snap63
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_multi_line_fix.snap114
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_incompatible_type_for_trait.snap48
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_mismatched_type.snap48
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_unused_variable.snap86
-rw-r--r--crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_wrong_number_of_parameters.snap67
-rw-r--r--crates/ra_flycheck/src/conv/test.rs1072
-rw-r--r--crates/ra_flycheck/src/lib.rs374
12 files changed, 2360 insertions, 0 deletions
diff --git a/crates/ra_flycheck/Cargo.toml b/crates/ra_flycheck/Cargo.toml
new file mode 100644
index 000000000..c9a9ddc12
--- /dev/null
+++ b/crates/ra_flycheck/Cargo.toml
@@ -0,0 +1,16 @@
1[package]
2edition = "2018"
3name = "ra_flycheck"
4version = "0.1.0"
5authors = ["rust-analyzer developers"]
6
7[dependencies]
8crossbeam-channel = "0.4.0"
9lsp-types = { version = "0.73.0", features = ["proposed"] }
10log = "0.4.8"
11cargo_metadata = "0.9.1"
12serde_json = "1.0.48"
13jod-thread = "0.1.1"
14
15[dev-dependencies]
16insta = "0.15.0"
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}
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_clippy_pass_by_ref.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_clippy_pass_by_ref.snap
new file mode 100644
index 000000000..4c9db0385
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_clippy_pass_by_ref.snap
@@ -0,0 +1,83 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/compiler/mir/tagset.rs",
9 range: Range {
10 start: Position {
11 line: 41,
12 character: 23,
13 },
14 end: Position {
15 line: 41,
16 character: 28,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 41,
24 character: 23,
25 },
26 end: Position {
27 line: 41,
28 character: 28,
29 },
30 },
31 severity: Some(
32 Warning,
33 ),
34 code: Some(
35 String(
36 "trivially_copy_pass_by_ref",
37 ),
38 ),
39 source: Some(
40 "clippy",
41 ),
42 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",
43 related_information: Some(
44 [
45 DiagnosticRelatedInformation {
46 location: Location {
47 uri: "file:///test/compiler/lib.rs",
48 range: Range {
49 start: Position {
50 line: 0,
51 character: 8,
52 },
53 end: Position {
54 line: 0,
55 character: 19,
56 },
57 },
58 },
59 message: "lint level defined here",
60 },
61 DiagnosticRelatedInformation {
62 location: Location {
63 uri: "file:///test/compiler/mir/tagset.rs",
64 range: Range {
65 start: Position {
66 line: 41,
67 character: 23,
68 },
69 end: Position {
70 line: 41,
71 character: 28,
72 },
73 },
74 },
75 message: "consider passing by value instead",
76 },
77 ],
78 ),
79 tags: None,
80 },
81 fixes: [],
82 },
83]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_handles_macro_location.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_handles_macro_location.snap
new file mode 100644
index 000000000..7cde4d867
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_handles_macro_location.snap
@@ -0,0 +1,48 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/src/main.rs",
9 range: Range {
10 start: Position {
11 line: 1,
12 character: 4,
13 },
14 end: Position {
15 line: 1,
16 character: 26,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 1,
24 character: 4,
25 },
26 end: Position {
27 line: 1,
28 character: 26,
29 },
30 },
31 severity: Some(
32 Error,
33 ),
34 code: Some(
35 String(
36 "E0277",
37 ),
38 ),
39 source: Some(
40 "rustc",
41 ),
42 message: "can\'t compare `{integer}` with `&str`\nthe trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
43 related_information: None,
44 tags: None,
45 },
46 fixes: [],
47 },
48]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_macro_compiler_error.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_macro_compiler_error.snap
new file mode 100644
index 000000000..1cc37e087
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_macro_compiler_error.snap
@@ -0,0 +1,63 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/crates/ra_hir_def/src/data.rs",
9 range: Range {
10 start: Position {
11 line: 79,
12 character: 15,
13 },
14 end: Position {
15 line: 79,
16 character: 41,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 79,
24 character: 15,
25 },
26 end: Position {
27 line: 79,
28 character: 41,
29 },
30 },
31 severity: Some(
32 Error,
33 ),
34 code: None,
35 source: Some(
36 "rustc",
37 ),
38 message: "Please register your known path in the path module",
39 related_information: Some(
40 [
41 DiagnosticRelatedInformation {
42 location: Location {
43 uri: "file:///test/crates/ra_hir_def/src/path.rs",
44 range: Range {
45 start: Position {
46 line: 264,
47 character: 8,
48 },
49 end: Position {
50 line: 264,
51 character: 76,
52 },
53 },
54 },
55 message: "Error originated from macro here",
56 },
57 ],
58 ),
59 tags: None,
60 },
61 fixes: [],
62 },
63]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_multi_line_fix.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_multi_line_fix.snap
new file mode 100644
index 000000000..615ed8378
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_multi_line_fix.snap
@@ -0,0 +1,114 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/src/main.rs",
9 range: Range {
10 start: Position {
11 line: 3,
12 character: 4,
13 },
14 end: Position {
15 line: 3,
16 character: 5,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 3,
24 character: 4,
25 },
26 end: Position {
27 line: 3,
28 character: 5,
29 },
30 },
31 severity: Some(
32 Warning,
33 ),
34 code: Some(
35 String(
36 "let_and_return",
37 ),
38 ),
39 source: Some(
40 "clippy",
41 ),
42 message: "returning the result of a let binding from a block\n`#[warn(clippy::let_and_return)]` on by default\nfor further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return",
43 related_information: Some(
44 [
45 DiagnosticRelatedInformation {
46 location: Location {
47 uri: "file:///test/src/main.rs",
48 range: Range {
49 start: Position {
50 line: 2,
51 character: 4,
52 },
53 end: Position {
54 line: 2,
55 character: 30,
56 },
57 },
58 },
59 message: "unnecessary let binding",
60 },
61 ],
62 ),
63 tags: None,
64 },
65 fixes: [
66 CodeAction {
67 title: "return the expression directly",
68 kind: Some(
69 "quickfix",
70 ),
71 diagnostics: None,
72 edit: Some(
73 WorkspaceEdit {
74 changes: Some(
75 {
76 "file:///test/src/main.rs": [
77 TextEdit {
78 range: Range {
79 start: Position {
80 line: 2,
81 character: 4,
82 },
83 end: Position {
84 line: 2,
85 character: 30,
86 },
87 },
88 new_text: "",
89 },
90 TextEdit {
91 range: Range {
92 start: Position {
93 line: 3,
94 character: 4,
95 },
96 end: Position {
97 line: 3,
98 character: 5,
99 },
100 },
101 new_text: "(0..10).collect()",
102 },
103 ],
104 },
105 ),
106 document_changes: None,
107 },
108 ),
109 command: None,
110 is_preferred: None,
111 },
112 ],
113 },
114]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_incompatible_type_for_trait.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_incompatible_type_for_trait.snap
new file mode 100644
index 000000000..0df0fce18
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_incompatible_type_for_trait.snap
@@ -0,0 +1,48 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/compiler/ty/list_iter.rs",
9 range: Range {
10 start: Position {
11 line: 51,
12 character: 4,
13 },
14 end: Position {
15 line: 51,
16 character: 47,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 51,
24 character: 4,
25 },
26 end: Position {
27 line: 51,
28 character: 47,
29 },
30 },
31 severity: Some(
32 Error,
33 ),
34 code: Some(
35 String(
36 "E0053",
37 ),
38 ),
39 source: Some(
40 "rustc",
41 ),
42 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>>`",
43 related_information: None,
44 tags: None,
45 },
46 fixes: [],
47 },
48]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_mismatched_type.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_mismatched_type.snap
new file mode 100644
index 000000000..28ebcb3b3
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_mismatched_type.snap
@@ -0,0 +1,48 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/runtime/compiler_support.rs",
9 range: Range {
10 start: Position {
11 line: 47,
12 character: 64,
13 },
14 end: Position {
15 line: 47,
16 character: 69,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 47,
24 character: 64,
25 },
26 end: Position {
27 line: 47,
28 character: 69,
29 },
30 },
31 severity: Some(
32 Error,
33 ),
34 code: Some(
35 String(
36 "E0308",
37 ),
38 ),
39 source: Some(
40 "rustc",
41 ),
42 message: "mismatched types\nexpected usize, found u32",
43 related_information: None,
44 tags: None,
45 },
46 fixes: [],
47 },
48]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_unused_variable.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_unused_variable.snap
new file mode 100644
index 000000000..5e0873281
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_unused_variable.snap
@@ -0,0 +1,86 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/driver/subcommand/repl.rs",
9 range: Range {
10 start: Position {
11 line: 290,
12 character: 8,
13 },
14 end: Position {
15 line: 290,
16 character: 11,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 290,
24 character: 8,
25 },
26 end: Position {
27 line: 290,
28 character: 11,
29 },
30 },
31 severity: Some(
32 Warning,
33 ),
34 code: Some(
35 String(
36 "unused_variables",
37 ),
38 ),
39 source: Some(
40 "rustc",
41 ),
42 message: "unused variable: `foo`\n#[warn(unused_variables)] on by default",
43 related_information: None,
44 tags: Some(
45 [
46 Unnecessary,
47 ],
48 ),
49 },
50 fixes: [
51 CodeAction {
52 title: "consider prefixing with an underscore",
53 kind: Some(
54 "quickfix",
55 ),
56 diagnostics: None,
57 edit: Some(
58 WorkspaceEdit {
59 changes: Some(
60 {
61 "file:///test/driver/subcommand/repl.rs": [
62 TextEdit {
63 range: Range {
64 start: Position {
65 line: 290,
66 character: 8,
67 },
68 end: Position {
69 line: 290,
70 character: 11,
71 },
72 },
73 new_text: "_foo",
74 },
75 ],
76 },
77 ),
78 document_changes: None,
79 },
80 ),
81 command: None,
82 is_preferred: None,
83 },
84 ],
85 },
86]
diff --git a/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_wrong_number_of_parameters.snap b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_wrong_number_of_parameters.snap
new file mode 100644
index 000000000..e500d3cd6
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/snapshots/ra_flycheck__conv__test__snap_rustc_wrong_number_of_parameters.snap
@@ -0,0 +1,67 @@
1---
2source: crates/ra_flycheck/src/conv/test.rs
3expression: diag
4---
5[
6 MappedRustDiagnostic {
7 location: Location {
8 uri: "file:///test/compiler/ty/select.rs",
9 range: Range {
10 start: Position {
11 line: 103,
12 character: 17,
13 },
14 end: Position {
15 line: 103,
16 character: 29,
17 },
18 },
19 },
20 diagnostic: Diagnostic {
21 range: Range {
22 start: Position {
23 line: 103,
24 character: 17,
25 },
26 end: Position {
27 line: 103,
28 character: 29,
29 },
30 },
31 severity: Some(
32 Error,
33 ),
34 code: Some(
35 String(
36 "E0061",
37 ),
38 ),
39 source: Some(
40 "rustc",
41 ),
42 message: "this function takes 2 parameters but 3 parameters were supplied\nexpected 2 parameters",
43 related_information: Some(
44 [
45 DiagnosticRelatedInformation {
46 location: Location {
47 uri: "file:///test/compiler/ty/select.rs",
48 range: Range {
49 start: Position {
50 line: 218,
51 character: 4,
52 },
53 end: Position {
54 line: 230,
55 character: 5,
56 },
57 },
58 },
59 message: "defined here",
60 },
61 ],
62 ),
63 tags: None,
64 },
65 fixes: [],
66 },
67]
diff --git a/crates/ra_flycheck/src/conv/test.rs b/crates/ra_flycheck/src/conv/test.rs
new file mode 100644
index 000000000..4e81455ca
--- /dev/null
+++ b/crates/ra_flycheck/src/conv/test.rs
@@ -0,0 +1,1072 @@
1//! This module contains the large and verbose snapshot tests for the
2//! conversions between `cargo check` json and LSP diagnostics.
3#[cfg(not(windows))]
4use crate::*;
5
6#[cfg(not(windows))]
7fn parse_diagnostic(val: &str) -> cargo_metadata::diagnostic::Diagnostic {
8 serde_json::from_str::<cargo_metadata::diagnostic::Diagnostic>(val).unwrap()
9}
10
11#[test]
12#[cfg(not(windows))]
13fn snap_rustc_incompatible_type_for_trait() {
14 let diag = parse_diagnostic(
15 r##"{
16 "message": "method `next` has an incompatible type for trait",
17 "code": {
18 "code": "E0053",
19 "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"
20 },
21 "level": "error",
22 "spans": [
23 {
24 "file_name": "compiler/ty/list_iter.rs",
25 "byte_start": 1307,
26 "byte_end": 1350,
27 "line_start": 52,
28 "line_end": 52,
29 "column_start": 5,
30 "column_end": 48,
31 "is_primary": true,
32 "text": [
33 {
34 "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {",
35 "highlight_start": 5,
36 "highlight_end": 48
37 }
38 ],
39 "label": "types differ in mutability",
40 "suggested_replacement": null,
41 "suggestion_applicability": null,
42 "expansion": null
43 }
44 ],
45 "children": [
46 {
47 "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>>`",
48 "code": null,
49 "level": "note",
50 "spans": [],
51 "children": [],
52 "rendered": null
53 }
54 ],
55 "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"
56 }
57 "##,
58 );
59
60 let workspace_root = PathBuf::from("/test/");
61 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
62 insta::assert_debug_snapshot!(diag);
63}
64
65#[test]
66#[cfg(not(windows))]
67fn snap_rustc_unused_variable() {
68 let diag = parse_diagnostic(
69 r##"{
70"message": "unused variable: `foo`",
71"code": {
72 "code": "unused_variables",
73 "explanation": null
74},
75"level": "warning",
76"spans": [
77 {
78 "file_name": "driver/subcommand/repl.rs",
79 "byte_start": 9228,
80 "byte_end": 9231,
81 "line_start": 291,
82 "line_end": 291,
83 "column_start": 9,
84 "column_end": 12,
85 "is_primary": true,
86 "text": [
87 {
88 "text": " let foo = 42;",
89 "highlight_start": 9,
90 "highlight_end": 12
91 }
92 ],
93 "label": null,
94 "suggested_replacement": null,
95 "suggestion_applicability": null,
96 "expansion": null
97 }
98],
99"children": [
100 {
101 "message": "#[warn(unused_variables)] on by default",
102 "code": null,
103 "level": "note",
104 "spans": [],
105 "children": [],
106 "rendered": null
107 },
108 {
109 "message": "consider prefixing with an underscore",
110 "code": null,
111 "level": "help",
112 "spans": [
113 {
114 "file_name": "driver/subcommand/repl.rs",
115 "byte_start": 9228,
116 "byte_end": 9231,
117 "line_start": 291,
118 "line_end": 291,
119 "column_start": 9,
120 "column_end": 12,
121 "is_primary": true,
122 "text": [
123 {
124 "text": " let foo = 42;",
125 "highlight_start": 9,
126 "highlight_end": 12
127 }
128 ],
129 "label": null,
130 "suggested_replacement": "_foo",
131 "suggestion_applicability": "MachineApplicable",
132 "expansion": null
133 }
134 ],
135 "children": [],
136 "rendered": null
137 }
138],
139"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"
140}"##,
141 );
142
143 let workspace_root = PathBuf::from("/test/");
144 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
145 insta::assert_debug_snapshot!(diag);
146}
147
148#[test]
149#[cfg(not(windows))]
150fn snap_rustc_wrong_number_of_parameters() {
151 let diag = parse_diagnostic(
152 r##"{
153"message": "this function takes 2 parameters but 3 parameters were supplied",
154"code": {
155 "code": "E0061",
156 "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"
157},
158"level": "error",
159"spans": [
160 {
161 "file_name": "compiler/ty/select.rs",
162 "byte_start": 8787,
163 "byte_end": 9241,
164 "line_start": 219,
165 "line_end": 231,
166 "column_start": 5,
167 "column_end": 6,
168 "is_primary": false,
169 "text": [
170 {
171 "text": " pub fn add_evidence(",
172 "highlight_start": 5,
173 "highlight_end": 25
174 },
175 {
176 "text": " &mut self,",
177 "highlight_start": 1,
178 "highlight_end": 19
179 },
180 {
181 "text": " target_poly: &ty::Ref<ty::Poly>,",
182 "highlight_start": 1,
183 "highlight_end": 41
184 },
185 {
186 "text": " evidence_poly: &ty::Ref<ty::Poly>,",
187 "highlight_start": 1,
188 "highlight_end": 43
189 },
190 {
191 "text": " ) {",
192 "highlight_start": 1,
193 "highlight_end": 8
194 },
195 {
196 "text": " match target_poly {",
197 "highlight_start": 1,
198 "highlight_end": 28
199 },
200 {
201 "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
202 "highlight_start": 1,
203 "highlight_end": 81
204 },
205 {
206 "text": " ty::Ref::Fixed(target_ty) => {",
207 "highlight_start": 1,
208 "highlight_end": 43
209 },
210 {
211 "text": " let evidence_ty = evidence_poly.resolve_to_ty();",
212 "highlight_start": 1,
213 "highlight_end": 65
214 },
215 {
216 "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
217 "highlight_start": 1,
218 "highlight_end": 76
219 },
220 {
221 "text": " }",
222 "highlight_start": 1,
223 "highlight_end": 14
224 },
225 {
226 "text": " }",
227 "highlight_start": 1,
228 "highlight_end": 10
229 },
230 {
231 "text": " }",
232 "highlight_start": 1,
233 "highlight_end": 6
234 }
235 ],
236 "label": "defined here",
237 "suggested_replacement": null,
238 "suggestion_applicability": null,
239 "expansion": null
240 },
241 {
242 "file_name": "compiler/ty/select.rs",
243 "byte_start": 4045,
244 "byte_end": 4057,
245 "line_start": 104,
246 "line_end": 104,
247 "column_start": 18,
248 "column_end": 30,
249 "is_primary": true,
250 "text": [
251 {
252 "text": " self.add_evidence(target_fixed, evidence_fixed, false);",
253 "highlight_start": 18,
254 "highlight_end": 30
255 }
256 ],
257 "label": "expected 2 parameters",
258 "suggested_replacement": null,
259 "suggestion_applicability": null,
260 "expansion": null
261 }
262],
263"children": [],
264"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"
265}"##,
266 );
267
268 let workspace_root = PathBuf::from("/test/");
269 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
270 insta::assert_debug_snapshot!(diag);
271}
272
273#[test]
274#[cfg(not(windows))]
275fn snap_clippy_pass_by_ref() {
276 let diag = parse_diagnostic(
277 r##"{
278"message": "this argument is passed by reference, but would be more efficient if passed by value",
279"code": {
280 "code": "clippy::trivially_copy_pass_by_ref",
281 "explanation": null
282},
283"level": "warning",
284"spans": [
285 {
286 "file_name": "compiler/mir/tagset.rs",
287 "byte_start": 941,
288 "byte_end": 946,
289 "line_start": 42,
290 "line_end": 42,
291 "column_start": 24,
292 "column_end": 29,
293 "is_primary": true,
294 "text": [
295 {
296 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
297 "highlight_start": 24,
298 "highlight_end": 29
299 }
300 ],
301 "label": null,
302 "suggested_replacement": null,
303 "suggestion_applicability": null,
304 "expansion": null
305 }
306],
307"children": [
308 {
309 "message": "lint level defined here",
310 "code": null,
311 "level": "note",
312 "spans": [
313 {
314 "file_name": "compiler/lib.rs",
315 "byte_start": 8,
316 "byte_end": 19,
317 "line_start": 1,
318 "line_end": 1,
319 "column_start": 9,
320 "column_end": 20,
321 "is_primary": true,
322 "text": [
323 {
324 "text": "#![warn(clippy::all)]",
325 "highlight_start": 9,
326 "highlight_end": 20
327 }
328 ],
329 "label": null,
330 "suggested_replacement": null,
331 "suggestion_applicability": null,
332 "expansion": null
333 }
334 ],
335 "children": [],
336 "rendered": null
337 },
338 {
339 "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
340 "code": null,
341 "level": "note",
342 "spans": [],
343 "children": [],
344 "rendered": null
345 },
346 {
347 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
348 "code": null,
349 "level": "help",
350 "spans": [],
351 "children": [],
352 "rendered": null
353 },
354 {
355 "message": "consider passing by value instead",
356 "code": null,
357 "level": "help",
358 "spans": [
359 {
360 "file_name": "compiler/mir/tagset.rs",
361 "byte_start": 941,
362 "byte_end": 946,
363 "line_start": 42,
364 "line_end": 42,
365 "column_start": 24,
366 "column_end": 29,
367 "is_primary": true,
368 "text": [
369 {
370 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
371 "highlight_start": 24,
372 "highlight_end": 29
373 }
374 ],
375 "label": null,
376 "suggested_replacement": "self",
377 "suggestion_applicability": "Unspecified",
378 "expansion": null
379 }
380 ],
381 "children": [],
382 "rendered": null
383 }
384],
385"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"
386}"##,
387 );
388
389 let workspace_root = PathBuf::from("/test/");
390 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
391 insta::assert_debug_snapshot!(diag);
392}
393
394#[test]
395#[cfg(not(windows))]
396fn snap_rustc_mismatched_type() {
397 let diag = parse_diagnostic(
398 r##"{
399"message": "mismatched types",
400"code": {
401 "code": "E0308",
402 "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"
403},
404"level": "error",
405"spans": [
406 {
407 "file_name": "runtime/compiler_support.rs",
408 "byte_start": 1589,
409 "byte_end": 1594,
410 "line_start": 48,
411 "line_end": 48,
412 "column_start": 65,
413 "column_end": 70,
414 "is_primary": true,
415 "text": [
416 {
417 "text": " let layout = alloc::Layout::from_size_align_unchecked(size, align);",
418 "highlight_start": 65,
419 "highlight_end": 70
420 }
421 ],
422 "label": "expected usize, found u32",
423 "suggested_replacement": null,
424 "suggestion_applicability": null,
425 "expansion": null
426 }
427],
428"children": [],
429"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"
430}"##,
431 );
432
433 let workspace_root = PathBuf::from("/test/");
434 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
435 insta::assert_debug_snapshot!(diag);
436}
437
438#[test]
439#[cfg(not(windows))]
440fn snap_handles_macro_location() {
441 let diag = parse_diagnostic(
442 r##"{
443"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",
444"children": [
445 {
446 "children": [],
447 "code": null,
448 "level": "help",
449 "message": "the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
450 "rendered": null,
451 "spans": []
452 }
453],
454"code": {
455 "code": "E0277",
456 "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"
457},
458"level": "error",
459"message": "can't compare `{integer}` with `&str`",
460"spans": [
461 {
462 "byte_end": 155,
463 "byte_start": 153,
464 "column_end": 33,
465 "column_start": 31,
466 "expansion": {
467 "def_site_span": {
468 "byte_end": 940,
469 "byte_start": 0,
470 "column_end": 6,
471 "column_start": 1,
472 "expansion": null,
473 "file_name": "<::core::macros::assert_eq macros>",
474 "is_primary": false,
475 "label": null,
476 "line_end": 36,
477 "line_start": 1,
478 "suggested_replacement": null,
479 "suggestion_applicability": null,
480 "text": [
481 {
482 "highlight_end": 35,
483 "highlight_start": 1,
484 "text": "($ left : expr, $ right : expr) =>"
485 },
486 {
487 "highlight_end": 3,
488 "highlight_start": 1,
489 "text": "({"
490 },
491 {
492 "highlight_end": 33,
493 "highlight_start": 1,
494 "text": " match (& $ left, & $ right)"
495 },
496 {
497 "highlight_end": 7,
498 "highlight_start": 1,
499 "text": " {"
500 },
501 {
502 "highlight_end": 34,
503 "highlight_start": 1,
504 "text": " (left_val, right_val) =>"
505 },
506 {
507 "highlight_end": 11,
508 "highlight_start": 1,
509 "text": " {"
510 },
511 {
512 "highlight_end": 46,
513 "highlight_start": 1,
514 "text": " if ! (* left_val == * right_val)"
515 },
516 {
517 "highlight_end": 15,
518 "highlight_start": 1,
519 "text": " {"
520 },
521 {
522 "highlight_end": 25,
523 "highlight_start": 1,
524 "text": " panic !"
525 },
526 {
527 "highlight_end": 57,
528 "highlight_start": 1,
529 "text": " (r#\"assertion failed: `(left == right)`"
530 },
531 {
532 "highlight_end": 16,
533 "highlight_start": 1,
534 "text": " left: `{:?}`,"
535 },
536 {
537 "highlight_end": 18,
538 "highlight_start": 1,
539 "text": " right: `{:?}`\"#,"
540 },
541 {
542 "highlight_end": 47,
543 "highlight_start": 1,
544 "text": " & * left_val, & * right_val)"
545 },
546 {
547 "highlight_end": 15,
548 "highlight_start": 1,
549 "text": " }"
550 },
551 {
552 "highlight_end": 11,
553 "highlight_start": 1,
554 "text": " }"
555 },
556 {
557 "highlight_end": 7,
558 "highlight_start": 1,
559 "text": " }"
560 },
561 {
562 "highlight_end": 42,
563 "highlight_start": 1,
564 "text": " }) ; ($ left : expr, $ right : expr,) =>"
565 },
566 {
567 "highlight_end": 49,
568 "highlight_start": 1,
569 "text": "({ $ crate :: assert_eq ! ($ left, $ right) }) ;"
570 },
571 {
572 "highlight_end": 53,
573 "highlight_start": 1,
574 "text": "($ left : expr, $ right : expr, $ ($ arg : tt) +) =>"
575 },
576 {
577 "highlight_end": 3,
578 "highlight_start": 1,
579 "text": "({"
580 },
581 {
582 "highlight_end": 37,
583 "highlight_start": 1,
584 "text": " match (& ($ left), & ($ right))"
585 },
586 {
587 "highlight_end": 7,
588 "highlight_start": 1,
589 "text": " {"
590 },
591 {
592 "highlight_end": 34,
593 "highlight_start": 1,
594 "text": " (left_val, right_val) =>"
595 },
596 {
597 "highlight_end": 11,
598 "highlight_start": 1,
599 "text": " {"
600 },
601 {
602 "highlight_end": 46,
603 "highlight_start": 1,
604 "text": " if ! (* left_val == * right_val)"
605 },
606 {
607 "highlight_end": 15,
608 "highlight_start": 1,
609 "text": " {"
610 },
611 {
612 "highlight_end": 25,
613 "highlight_start": 1,
614 "text": " panic !"
615 },
616 {
617 "highlight_end": 57,
618 "highlight_start": 1,
619 "text": " (r#\"assertion failed: `(left == right)`"
620 },
621 {
622 "highlight_end": 16,
623 "highlight_start": 1,
624 "text": " left: `{:?}`,"
625 },
626 {
627 "highlight_end": 22,
628 "highlight_start": 1,
629 "text": " right: `{:?}`: {}\"#,"
630 },
631 {
632 "highlight_end": 72,
633 "highlight_start": 1,
634 "text": " & * left_val, & * right_val, $ crate :: format_args !"
635 },
636 {
637 "highlight_end": 33,
638 "highlight_start": 1,
639 "text": " ($ ($ arg) +))"
640 },
641 {
642 "highlight_end": 15,
643 "highlight_start": 1,
644 "text": " }"
645 },
646 {
647 "highlight_end": 11,
648 "highlight_start": 1,
649 "text": " }"
650 },
651 {
652 "highlight_end": 7,
653 "highlight_start": 1,
654 "text": " }"
655 },
656 {
657 "highlight_end": 6,
658 "highlight_start": 1,
659 "text": " }) ;"
660 }
661 ]
662 },
663 "macro_decl_name": "assert_eq!",
664 "span": {
665 "byte_end": 38,
666 "byte_start": 16,
667 "column_end": 27,
668 "column_start": 5,
669 "expansion": null,
670 "file_name": "src/main.rs",
671 "is_primary": false,
672 "label": null,
673 "line_end": 2,
674 "line_start": 2,
675 "suggested_replacement": null,
676 "suggestion_applicability": null,
677 "text": [
678 {
679 "highlight_end": 27,
680 "highlight_start": 5,
681 "text": " assert_eq!(1, \"love\");"
682 }
683 ]
684 }
685 },
686 "file_name": "<::core::macros::assert_eq macros>",
687 "is_primary": true,
688 "label": "no implementation for `{integer} == &str`",
689 "line_end": 7,
690 "line_start": 7,
691 "suggested_replacement": null,
692 "suggestion_applicability": null,
693 "text": [
694 {
695 "highlight_end": 33,
696 "highlight_start": 31,
697 "text": " if ! (* left_val == * right_val)"
698 }
699 ]
700 }
701]
702}"##,
703 );
704
705 let workspace_root = PathBuf::from("/test/");
706 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
707 insta::assert_debug_snapshot!(diag);
708}
709
710#[test]
711#[cfg(not(windows))]
712fn snap_macro_compiler_error() {
713 let diag = parse_diagnostic(
714 r##"{
715 "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",
716 "children": [],
717 "code": null,
718 "level": "error",
719 "message": "Please register your known path in the path module",
720 "spans": [
721 {
722 "byte_end": 8285,
723 "byte_start": 8217,
724 "column_end": 77,
725 "column_start": 9,
726 "expansion": {
727 "def_site_span": {
728 "byte_end": 8294,
729 "byte_start": 7858,
730 "column_end": 2,
731 "column_start": 1,
732 "expansion": null,
733 "file_name": "crates/ra_hir_def/src/path.rs",
734 "is_primary": false,
735 "label": null,
736 "line_end": 267,
737 "line_start": 254,
738 "suggested_replacement": null,
739 "suggestion_applicability": null,
740 "text": [
741 {
742 "highlight_end": 28,
743 "highlight_start": 1,
744 "text": "macro_rules! __known_path {"
745 },
746 {
747 "highlight_end": 37,
748 "highlight_start": 1,
749 "text": " (std::iter::IntoIterator) => {};"
750 },
751 {
752 "highlight_end": 33,
753 "highlight_start": 1,
754 "text": " (std::result::Result) => {};"
755 },
756 {
757 "highlight_end": 29,
758 "highlight_start": 1,
759 "text": " (std::ops::Range) => {};"
760 },
761 {
762 "highlight_end": 33,
763 "highlight_start": 1,
764 "text": " (std::ops::RangeFrom) => {};"
765 },
766 {
767 "highlight_end": 33,
768 "highlight_start": 1,
769 "text": " (std::ops::RangeFull) => {};"
770 },
771 {
772 "highlight_end": 31,
773 "highlight_start": 1,
774 "text": " (std::ops::RangeTo) => {};"
775 },
776 {
777 "highlight_end": 40,
778 "highlight_start": 1,
779 "text": " (std::ops::RangeToInclusive) => {};"
780 },
781 {
782 "highlight_end": 38,
783 "highlight_start": 1,
784 "text": " (std::ops::RangeInclusive) => {};"
785 },
786 {
787 "highlight_end": 27,
788 "highlight_start": 1,
789 "text": " (std::ops::Try) => {};"
790 },
791 {
792 "highlight_end": 22,
793 "highlight_start": 1,
794 "text": " ($path:path) => {"
795 },
796 {
797 "highlight_end": 77,
798 "highlight_start": 1,
799 "text": " compile_error!(\"Please register your known path in the path module\")"
800 },
801 {
802 "highlight_end": 7,
803 "highlight_start": 1,
804 "text": " };"
805 },
806 {
807 "highlight_end": 2,
808 "highlight_start": 1,
809 "text": "}"
810 }
811 ]
812 },
813 "macro_decl_name": "$crate::__known_path!",
814 "span": {
815 "byte_end": 8427,
816 "byte_start": 8385,
817 "column_end": 51,
818 "column_start": 9,
819 "expansion": {
820 "def_site_span": {
821 "byte_end": 8611,
822 "byte_start": 8312,
823 "column_end": 2,
824 "column_start": 1,
825 "expansion": null,
826 "file_name": "crates/ra_hir_def/src/path.rs",
827 "is_primary": false,
828 "label": null,
829 "line_end": 277,
830 "line_start": 270,
831 "suggested_replacement": null,
832 "suggestion_applicability": null,
833 "text": [
834 {
835 "highlight_end": 22,
836 "highlight_start": 1,
837 "text": "macro_rules! __path {"
838 },
839 {
840 "highlight_end": 43,
841 "highlight_start": 1,
842 "text": " ($start:ident $(:: $seg:ident)*) => ({"
843 },
844 {
845 "highlight_end": 51,
846 "highlight_start": 1,
847 "text": " $crate::__known_path!($start $(:: $seg)*);"
848 },
849 {
850 "highlight_end": 87,
851 "highlight_start": 1,
852 "text": " $crate::path::ModPath::from_simple_segments($crate::path::PathKind::Abs, vec!["
853 },
854 {
855 "highlight_end": 76,
856 "highlight_start": 1,
857 "text": " $crate::path::__name![$start], $($crate::path::__name![$seg],)*"
858 },
859 {
860 "highlight_end": 11,
861 "highlight_start": 1,
862 "text": " ])"
863 },
864 {
865 "highlight_end": 8,
866 "highlight_start": 1,
867 "text": " });"
868 },
869 {
870 "highlight_end": 2,
871 "highlight_start": 1,
872 "text": "}"
873 }
874 ]
875 },
876 "macro_decl_name": "path!",
877 "span": {
878 "byte_end": 2966,
879 "byte_start": 2940,
880 "column_end": 42,
881 "column_start": 16,
882 "expansion": null,
883 "file_name": "crates/ra_hir_def/src/data.rs",
884 "is_primary": false,
885 "label": null,
886 "line_end": 80,
887 "line_start": 80,
888 "suggested_replacement": null,
889 "suggestion_applicability": null,
890 "text": [
891 {
892 "highlight_end": 42,
893 "highlight_start": 16,
894 "text": " let path = path![std::future::Future];"
895 }
896 ]
897 }
898 },
899 "file_name": "crates/ra_hir_def/src/path.rs",
900 "is_primary": false,
901 "label": null,
902 "line_end": 272,
903 "line_start": 272,
904 "suggested_replacement": null,
905 "suggestion_applicability": null,
906 "text": [
907 {
908 "highlight_end": 51,
909 "highlight_start": 9,
910 "text": " $crate::__known_path!($start $(:: $seg)*);"
911 }
912 ]
913 }
914 },
915 "file_name": "crates/ra_hir_def/src/path.rs",
916 "is_primary": true,
917 "label": null,
918 "line_end": 265,
919 "line_start": 265,
920 "suggested_replacement": null,
921 "suggestion_applicability": null,
922 "text": [
923 {
924 "highlight_end": 77,
925 "highlight_start": 9,
926 "text": " compile_error!(\"Please register your known path in the path module\")"
927 }
928 ]
929 }
930 ]
931}
932 "##,
933 );
934
935 let workspace_root = PathBuf::from("/test/");
936 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
937 insta::assert_debug_snapshot!(diag);
938}
939
940#[test]
941#[cfg(not(windows))]
942fn snap_multi_line_fix() {
943 let diag = parse_diagnostic(
944 r##"{
945 "rendered": "warning: returning the result of a let binding from a block\n --> src/main.rs:4:5\n |\n3 | let a = (0..10).collect();\n | -------------------------- unnecessary let binding\n4 | a\n | ^\n |\n = note: `#[warn(clippy::let_and_return)]` on by default\n = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return\nhelp: return the expression directly\n |\n3 | \n4 | (0..10).collect()\n |\n\n",
946 "children": [
947 {
948 "children": [],
949 "code": null,
950 "level": "note",
951 "message": "`#[warn(clippy::let_and_return)]` on by default",
952 "rendered": null,
953 "spans": []
954 },
955 {
956 "children": [],
957 "code": null,
958 "level": "help",
959 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#let_and_return",
960 "rendered": null,
961 "spans": []
962 },
963 {
964 "children": [],
965 "code": null,
966 "level": "help",
967 "message": "return the expression directly",
968 "rendered": null,
969 "spans": [
970 {
971 "byte_end": 55,
972 "byte_start": 29,
973 "column_end": 31,
974 "column_start": 5,
975 "expansion": null,
976 "file_name": "src/main.rs",
977 "is_primary": true,
978 "label": null,
979 "line_end": 3,
980 "line_start": 3,
981 "suggested_replacement": "",
982 "suggestion_applicability": "MachineApplicable",
983 "text": [
984 {
985 "highlight_end": 31,
986 "highlight_start": 5,
987 "text": " let a = (0..10).collect();"
988 }
989 ]
990 },
991 {
992 "byte_end": 61,
993 "byte_start": 60,
994 "column_end": 6,
995 "column_start": 5,
996 "expansion": null,
997 "file_name": "src/main.rs",
998 "is_primary": true,
999 "label": null,
1000 "line_end": 4,
1001 "line_start": 4,
1002 "suggested_replacement": "(0..10).collect()",
1003 "suggestion_applicability": "MachineApplicable",
1004 "text": [
1005 {
1006 "highlight_end": 6,
1007 "highlight_start": 5,
1008 "text": " a"
1009 }
1010 ]
1011 }
1012 ]
1013 }
1014 ],
1015 "code": {
1016 "code": "clippy::let_and_return",
1017 "explanation": null
1018 },
1019 "level": "warning",
1020 "message": "returning the result of a let binding from a block",
1021 "spans": [
1022 {
1023 "byte_end": 55,
1024 "byte_start": 29,
1025 "column_end": 31,
1026 "column_start": 5,
1027 "expansion": null,
1028 "file_name": "src/main.rs",
1029 "is_primary": false,
1030 "label": "unnecessary let binding",
1031 "line_end": 3,
1032 "line_start": 3,
1033 "suggested_replacement": null,
1034 "suggestion_applicability": null,
1035 "text": [
1036 {
1037 "highlight_end": 31,
1038 "highlight_start": 5,
1039 "text": " let a = (0..10).collect();"
1040 }
1041 ]
1042 },
1043 {
1044 "byte_end": 61,
1045 "byte_start": 60,
1046 "column_end": 6,
1047 "column_start": 5,
1048 "expansion": null,
1049 "file_name": "src/main.rs",
1050 "is_primary": true,
1051 "label": null,
1052 "line_end": 4,
1053 "line_start": 4,
1054 "suggested_replacement": null,
1055 "suggestion_applicability": null,
1056 "text": [
1057 {
1058 "highlight_end": 6,
1059 "highlight_start": 5,
1060 "text": " a"
1061 }
1062 ]
1063 }
1064 ]
1065 }
1066 "##,
1067 );
1068
1069 let workspace_root = PathBuf::from("/test/");
1070 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root);
1071 insta::assert_debug_snapshot!(diag);
1072}
diff --git a/crates/ra_flycheck/src/lib.rs b/crates/ra_flycheck/src/lib.rs
new file mode 100644
index 000000000..77ede8f63
--- /dev/null
+++ b/crates/ra_flycheck/src/lib.rs
@@ -0,0 +1,374 @@
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 CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin,
8 WorkDoneProgressEnd, WorkDoneProgressReport,
9};
10use std::{
11 error, fmt,
12 io::{BufRead, BufReader},
13 path::{Path, PathBuf},
14 process::{Command, Stdio},
15 time::Instant,
16};
17
18mod conv;
19
20use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic};
21
22pub use crate::conv::url_from_path_with_drive_lowercasing;
23
24#[derive(Clone, Debug)]
25pub struct CheckConfig {
26 pub enable: bool,
27 pub args: Vec<String>,
28 pub command: String,
29 pub all_targets: bool,
30}
31
32/// CheckWatcher wraps the shared state and communication machinery used for
33/// running `cargo check` (or other compatible command) and providing
34/// diagnostics based on the output.
35/// The spawned thread is shut down when this struct is dropped.
36#[derive(Debug)]
37pub struct CheckWatcher {
38 // XXX: drop order is significant
39 cmd_send: Sender<CheckCommand>,
40 handle: Option<jod_thread::JoinHandle<()>>,
41 pub task_recv: Receiver<CheckTask>,
42}
43
44impl CheckWatcher {
45 pub fn new(config: CheckConfig, workspace_root: PathBuf) -> CheckWatcher {
46 let (task_send, task_recv) = unbounded::<CheckTask>();
47 let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
48 let handle = jod_thread::spawn(move || {
49 let mut check = CheckWatcherThread::new(config, workspace_root);
50 check.run(&task_send, &cmd_recv);
51 });
52 CheckWatcher { task_recv, cmd_send, handle: Some(handle) }
53 }
54
55 /// Schedule a re-start of the cargo check worker.
56 pub fn update(&self) {
57 self.cmd_send.send(CheckCommand::Update).unwrap();
58 }
59}
60
61#[derive(Debug)]
62pub enum CheckTask {
63 /// Request a clearing of all cached diagnostics from the check watcher
64 ClearDiagnostics,
65
66 /// Request adding a diagnostic with fixes included to a file
67 AddDiagnostic { url: Url, diagnostic: Diagnostic, fixes: Vec<CodeActionOrCommand> },
68
69 /// Request check progress notification to client
70 Status(WorkDoneProgress),
71}
72
73pub enum CheckCommand {
74 /// Request re-start of check thread
75 Update,
76}
77
78struct CheckWatcherThread {
79 options: CheckConfig,
80 workspace_root: PathBuf,
81 watcher: WatchThread,
82 last_update_req: Option<Instant>,
83}
84
85impl CheckWatcherThread {
86 fn new(options: CheckConfig, workspace_root: PathBuf) -> CheckWatcherThread {
87 CheckWatcherThread {
88 options,
89 workspace_root,
90 watcher: WatchThread::dummy(),
91 last_update_req: None,
92 }
93 }
94
95 fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
96 // If we rerun the thread, we need to discard the previous check results first
97 self.clean_previous_results(task_send);
98
99 loop {
100 select! {
101 recv(&cmd_recv) -> cmd => match cmd {
102 Ok(cmd) => self.handle_command(cmd),
103 Err(RecvError) => {
104 // Command channel has closed, so shut down
105 break;
106 },
107 },
108 recv(self.watcher.message_recv) -> msg => match msg {
109 Ok(msg) => self.handle_message(msg, task_send),
110 Err(RecvError) => {
111 // Watcher finished, replace it with a never channel to
112 // avoid busy-waiting.
113 std::mem::replace(&mut self.watcher.message_recv, never());
114 },
115 }
116 };
117
118 if self.should_recheck() {
119 self.last_update_req.take();
120 task_send.send(CheckTask::ClearDiagnostics).unwrap();
121
122 // Replace with a dummy watcher first so we drop the original and wait for completion
123 std::mem::replace(&mut self.watcher, WatchThread::dummy());
124
125 // Then create the actual new watcher
126 self.watcher = WatchThread::new(&self.options, &self.workspace_root);
127 }
128 }
129 }
130
131 fn clean_previous_results(&self, task_send: &Sender<CheckTask>) {
132 task_send.send(CheckTask::ClearDiagnostics).unwrap();
133 task_send
134 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd { message: None })))
135 .unwrap();
136 }
137
138 fn should_recheck(&mut self) -> bool {
139 if let Some(_last_update_req) = &self.last_update_req {
140 // We currently only request an update on save, as we need up to
141 // date source on disk for cargo check to do it's magic, so we
142 // don't really need to debounce the requests at this point.
143 return true;
144 }
145 false
146 }
147
148 fn handle_command(&mut self, cmd: CheckCommand) {
149 match cmd {
150 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
151 }
152 }
153
154 fn handle_message(&self, msg: CheckEvent, task_send: &Sender<CheckTask>) {
155 match msg {
156 CheckEvent::Begin => {
157 task_send
158 .send(CheckTask::Status(WorkDoneProgress::Begin(WorkDoneProgressBegin {
159 title: "Running 'cargo check'".to_string(),
160 cancellable: Some(false),
161 message: None,
162 percentage: None,
163 })))
164 .unwrap();
165 }
166
167 CheckEvent::End => {
168 task_send
169 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd {
170 message: None,
171 })))
172 .unwrap();
173 }
174
175 CheckEvent::Msg(Message::CompilerArtifact(msg)) => {
176 task_send
177 .send(CheckTask::Status(WorkDoneProgress::Report(WorkDoneProgressReport {
178 cancellable: Some(false),
179 message: Some(msg.target.name),
180 percentage: None,
181 })))
182 .unwrap();
183 }
184
185 CheckEvent::Msg(Message::CompilerMessage(msg)) => {
186 let map_result = map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root);
187 if map_result.is_empty() {
188 return;
189 }
190
191 for MappedRustDiagnostic { location, diagnostic, fixes } in map_result {
192 let fixes = fixes
193 .into_iter()
194 .map(|fix| {
195 CodeAction { diagnostics: Some(vec![diagnostic.clone()]), ..fix }.into()
196 })
197 .collect();
198
199 task_send
200 .send(CheckTask::AddDiagnostic { url: location.uri, diagnostic, fixes })
201 .unwrap();
202 }
203 }
204
205 CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
206 CheckEvent::Msg(Message::Unknown) => {}
207 }
208 }
209}
210
211#[derive(Debug)]
212pub struct DiagnosticWithFixes {
213 diagnostic: Diagnostic,
214 fixes: Vec<CodeAction>,
215}
216
217/// WatchThread exists to wrap around the communication needed to be able to
218/// run `cargo check` without blocking. Currently the Rust standard library
219/// doesn't provide a way to read sub-process output without blocking, so we
220/// have to wrap sub-processes output handling in a thread and pass messages
221/// back over a channel.
222/// The correct way to dispose of the thread is to drop it, on which the
223/// sub-process will be killed, and the thread will be joined.
224struct WatchThread {
225 // XXX: drop order is significant
226 message_recv: Receiver<CheckEvent>,
227 _handle: Option<jod_thread::JoinHandle<()>>,
228}
229
230enum CheckEvent {
231 Begin,
232 Msg(cargo_metadata::Message),
233 End,
234}
235
236#[derive(Debug)]
237pub struct CargoError(String);
238
239impl fmt::Display for CargoError {
240 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
241 write!(f, "Cargo failed: {}", self.0)
242 }
243}
244impl error::Error for CargoError {}
245
246pub fn run_cargo(
247 args: &[String],
248 current_dir: Option<&Path>,
249 on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool,
250) -> Result<(), CargoError> {
251 let mut command = Command::new("cargo");
252 if let Some(current_dir) = current_dir {
253 command.current_dir(current_dir);
254 }
255
256 let mut child = command
257 .args(args)
258 .stdout(Stdio::piped())
259 .stderr(Stdio::null())
260 .stdin(Stdio::null())
261 .spawn()
262 .expect("couldn't launch cargo");
263
264 // We manually read a line at a time, instead of using serde's
265 // stream deserializers, because the deserializer cannot recover
266 // from an error, resulting in it getting stuck, because we try to
267 // be resillient against failures.
268 //
269 // Because cargo only outputs one JSON object per line, we can
270 // simply skip a line if it doesn't parse, which just ignores any
271 // erroneus output.
272 let stdout = BufReader::new(child.stdout.take().unwrap());
273 let mut read_at_least_one_message = false;
274
275 for line in stdout.lines() {
276 let line = match line {
277 Ok(line) => line,
278 Err(err) => {
279 log::error!("Couldn't read line from cargo: {}", err);
280 continue;
281 }
282 };
283
284 let message = serde_json::from_str::<cargo_metadata::Message>(&line);
285 let message = match message {
286 Ok(message) => message,
287 Err(err) => {
288 log::error!("Invalid json from cargo check, ignoring ({}): {:?} ", err, line);
289 continue;
290 }
291 };
292
293 read_at_least_one_message = true;
294
295 if !on_message(message) {
296 break;
297 }
298 }
299
300 // It is okay to ignore the result, as it only errors if the process is already dead
301 let _ = child.kill();
302
303 let err_msg = match child.wait() {
304 Ok(exit_code) if !exit_code.success() && !read_at_least_one_message => {
305 // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
306 // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
307 format!(
308 "the command produced no valid metadata (exit code: {:?}): cargo {}",
309 exit_code,
310 args.join(" ")
311 )
312 }
313 Err(err) => format!("io error: {:?}", err),
314 Ok(_) => return Ok(()),
315 };
316
317 Err(CargoError(err_msg))
318}
319
320impl WatchThread {
321 fn dummy() -> WatchThread {
322 WatchThread { message_recv: never(), _handle: None }
323 }
324
325 fn new(options: &CheckConfig, workspace_root: &Path) -> WatchThread {
326 let mut args: Vec<String> = vec![
327 options.command.clone(),
328 "--workspace".to_string(),
329 "--message-format=json".to_string(),
330 "--manifest-path".to_string(),
331 format!("{}/Cargo.toml", workspace_root.display()),
332 ];
333 if options.all_targets {
334 args.push("--all-targets".to_string());
335 }
336 args.extend(options.args.iter().cloned());
337
338 let (message_send, message_recv) = unbounded();
339 let workspace_root = workspace_root.to_owned();
340 let handle = if options.enable {
341 Some(jod_thread::spawn(move || {
342 // If we trigger an error here, we will do so in the loop instead,
343 // which will break out of the loop, and continue the shutdown
344 let _ = message_send.send(CheckEvent::Begin);
345
346 let res = run_cargo(&args, Some(&workspace_root), &mut |message| {
347 // Skip certain kinds of messages to only spend time on what's useful
348 match &message {
349 Message::CompilerArtifact(artifact) if artifact.fresh => return true,
350 Message::BuildScriptExecuted(_) => return true,
351 Message::Unknown => return true,
352 _ => {}
353 }
354
355 // if the send channel was closed, we want to shutdown
356 message_send.send(CheckEvent::Msg(message)).is_ok()
357 });
358
359 if let Err(err) = res {
360 // FIXME: make the `message_send` to be `Sender<Result<CheckEvent, CargoError>>`
361 // to display user-caused misconfiguration errors instead of just logging them here
362 log::error!("Cargo watcher failed {:?}", err);
363 }
364
365 // We can ignore any error here, as we are already in the progress
366 // of shutting down.
367 let _ = message_send.send(CheckEvent::End);
368 }))
369 } else {
370 None
371 };
372 WatchThread { message_recv, _handle: handle }
373 }
374}