aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_cargo_watch
diff options
context:
space:
mode:
authorEmil Lauridsen <[email protected]>2019-12-27 10:10:07 +0000
committerEmil Lauridsen <[email protected]>2019-12-27 10:10:07 +0000
commit428a6ff5b8bad2c80a3522599195bf2a393f744e (patch)
tree9707dd343b8f00f01041a7995536aac84b137291 /crates/ra_cargo_watch
parent0cdbd0814958e174c5481d6bf16bd2a7e53ec981 (diff)
Move cargo watch functionality to separate crate
Diffstat (limited to 'crates/ra_cargo_watch')
-rw-r--r--crates/ra_cargo_watch/Cargo.toml17
-rw-r--r--crates/ra_cargo_watch/src/conv.rs280
-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_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.rs698
-rw-r--r--crates/ra_cargo_watch/src/lib.rs345
10 files changed, 1698 insertions, 0 deletions
diff --git a/crates/ra_cargo_watch/Cargo.toml b/crates/ra_cargo_watch/Cargo.toml
new file mode 100644
index 000000000..bcc4648ff
--- /dev/null
+++ b/crates/ra_cargo_watch/Cargo.toml
@@ -0,0 +1,17 @@
1[package]
2edition = "2018"
3name = "ra_cargo_watch"
4version = "0.1.0"
5authors = ["rust-analyzer developers"]
6
7[dependencies]
8crossbeam-channel = "0.4"
9lsp-types = { version = "0.67.0", features = ["proposed"] }
10log = "0.4.3"
11cargo_metadata = "0.9.1"
12jod-thread = "0.1.0"
13parking_lot = "0.10.0"
14
15[dev-dependencies]
16insta = "0.12.0"
17serde_json = "1.0" \ No newline at end of file
diff --git a/crates/ra_cargo_watch/src/conv.rs b/crates/ra_cargo_watch/src/conv.rs
new file mode 100644
index 000000000..3bd4bf7a5
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv.rs
@@ -0,0 +1,280 @@
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::{fmt::Write, path::PathBuf};
12
13#[cfg(test)]
14mod test;
15
16/// Converts a Rust level string to a LSP severity
17fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
18 match val {
19 DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error),
20 DiagnosticLevel::Error => Some(DiagnosticSeverity::Error),
21 DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning),
22 DiagnosticLevel::Note => Some(DiagnosticSeverity::Information),
23 DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint),
24 DiagnosticLevel::Unknown => None,
25 }
26}
27
28/// Check whether a file name is from macro invocation
29fn is_from_macro(file_name: &str) -> bool {
30 file_name.starts_with('<') && file_name.ends_with('>')
31}
32
33/// Converts a Rust macro span to a LSP location recursively
34fn map_macro_span_to_location(
35 span_macro: &DiagnosticSpanMacroExpansion,
36 workspace_root: &PathBuf,
37) -> Option<Location> {
38 if !is_from_macro(&span_macro.span.file_name) {
39 return Some(map_span_to_location(&span_macro.span, workspace_root));
40 }
41
42 if let Some(expansion) = &span_macro.span.expansion {
43 return map_macro_span_to_location(&expansion, workspace_root);
44 }
45
46 None
47}
48
49/// Converts a Rust span to a LSP location
50fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
51 if is_from_macro(&span.file_name) && span.expansion.is_some() {
52 let expansion = span.expansion.as_ref().unwrap();
53 if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) {
54 return macro_range;
55 }
56 }
57
58 let mut file_name = workspace_root.clone();
59 file_name.push(&span.file_name);
60 let uri = Url::from_file_path(file_name).unwrap();
61
62 let range = Range::new(
63 Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1),
64 Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1),
65 );
66
67 Location { uri, range }
68}
69
70/// Converts a secondary Rust span to a LSP related information
71///
72/// If the span is unlabelled this will return `None`.
73fn map_secondary_span_to_related(
74 span: &DiagnosticSpan,
75 workspace_root: &PathBuf,
76) -> Option<DiagnosticRelatedInformation> {
77 if let Some(label) = &span.label {
78 let location = map_span_to_location(span, workspace_root);
79 Some(DiagnosticRelatedInformation { location, message: label.clone() })
80 } else {
81 // Nothing to label this with
82 None
83 }
84}
85
86/// Determines if diagnostic is related to unused code
87fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool {
88 if let Some(code) = &rd.code {
89 match code.code.as_str() {
90 "dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes"
91 | "unused_imports" | "unused_macros" | "unused_variables" => true,
92 _ => false,
93 }
94 } else {
95 false
96 }
97}
98
99/// Determines if diagnostic is related to deprecated code
100fn is_deprecated(rd: &RustDiagnostic) -> bool {
101 if let Some(code) = &rd.code {
102 match code.code.as_str() {
103 "deprecated" => true,
104 _ => false,
105 }
106 } else {
107 false
108 }
109}
110
111#[derive(Debug)]
112pub struct SuggestedFix {
113 pub title: String,
114 pub location: Location,
115 pub replacement: String,
116 pub applicability: Applicability,
117 pub diagnostics: Vec<Diagnostic>,
118}
119
120impl std::cmp::PartialEq<SuggestedFix> for SuggestedFix {
121 fn eq(&self, other: &SuggestedFix) -> bool {
122 if self.title == other.title
123 && self.location == other.location
124 && self.replacement == other.replacement
125 {
126 // Applicability doesn't impl PartialEq...
127 match (&self.applicability, &other.applicability) {
128 (Applicability::MachineApplicable, Applicability::MachineApplicable) => true,
129 (Applicability::HasPlaceholders, Applicability::HasPlaceholders) => true,
130 (Applicability::MaybeIncorrect, Applicability::MaybeIncorrect) => true,
131 (Applicability::Unspecified, Applicability::Unspecified) => true,
132 _ => false,
133 }
134 } else {
135 false
136 }
137 }
138}
139
140enum MappedRustChildDiagnostic {
141 Related(DiagnosticRelatedInformation),
142 SuggestedFix(SuggestedFix),
143 MessageLine(String),
144}
145
146fn map_rust_child_diagnostic(
147 rd: &RustDiagnostic,
148 workspace_root: &PathBuf,
149) -> MappedRustChildDiagnostic {
150 let span: &DiagnosticSpan = match rd.spans.iter().find(|s| s.is_primary) {
151 Some(span) => span,
152 None => {
153 // `rustc` uses these spanless children as a way to print multi-line
154 // messages
155 return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
156 }
157 };
158
159 // If we have a primary span use its location, otherwise use the parent
160 let location = map_span_to_location(&span, workspace_root);
161
162 if let Some(suggested_replacement) = &span.suggested_replacement {
163 // Include our replacement in the title unless it's empty
164 let title = if !suggested_replacement.is_empty() {
165 format!("{}: '{}'", rd.message, suggested_replacement)
166 } else {
167 rd.message.clone()
168 };
169
170 MappedRustChildDiagnostic::SuggestedFix(SuggestedFix {
171 title,
172 location,
173 replacement: suggested_replacement.clone(),
174 applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown),
175 diagnostics: vec![],
176 })
177 } else {
178 MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
179 location,
180 message: rd.message.clone(),
181 })
182 }
183}
184
185#[derive(Debug)]
186pub(crate) struct MappedRustDiagnostic {
187 pub location: Location,
188 pub diagnostic: Diagnostic,
189 pub suggested_fixes: Vec<SuggestedFix>,
190}
191
192/// Converts a Rust root diagnostic to LSP form
193///
194/// This flattens the Rust diagnostic by:
195///
196/// 1. Creating a LSP diagnostic with the root message and primary span.
197/// 2. Adding any labelled secondary spans to `relatedInformation`
198/// 3. Categorising child diagnostics as either `SuggestedFix`es,
199/// `relatedInformation` or additional message lines.
200///
201/// If the diagnostic has no primary span this will return `None`
202pub(crate) fn map_rust_diagnostic_to_lsp(
203 rd: &RustDiagnostic,
204 workspace_root: &PathBuf,
205) -> Option<MappedRustDiagnostic> {
206 let primary_span = rd.spans.iter().find(|s| s.is_primary)?;
207
208 let location = map_span_to_location(&primary_span, workspace_root);
209
210 let severity = map_level_to_severity(rd.level);
211 let mut primary_span_label = primary_span.label.as_ref();
212
213 let mut source = String::from("rustc");
214 let mut code = rd.code.as_ref().map(|c| c.code.clone());
215 if let Some(code_val) = &code {
216 // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
217 let scoped_code: Vec<&str> = code_val.split("::").collect();
218 if scoped_code.len() == 2 {
219 source = String::from(scoped_code[0]);
220 code = Some(String::from(scoped_code[1]));
221 }
222 }
223
224 let mut related_information = vec![];
225 let mut tags = vec![];
226
227 for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) {
228 let related = map_secondary_span_to_related(secondary_span, workspace_root);
229 if let Some(related) = related {
230 related_information.push(related);
231 }
232 }
233
234 let mut suggested_fixes = vec![];
235 let mut message = rd.message.clone();
236 for child in &rd.children {
237 let child = map_rust_child_diagnostic(&child, workspace_root);
238 match child {
239 MappedRustChildDiagnostic::Related(related) => related_information.push(related),
240 MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => {
241 suggested_fixes.push(suggested_fix)
242 }
243 MappedRustChildDiagnostic::MessageLine(message_line) => {
244 write!(&mut message, "\n{}", message_line).unwrap();
245
246 // These secondary messages usually duplicate the content of the
247 // primary span label.
248 primary_span_label = None;
249 }
250 }
251 }
252
253 if let Some(primary_span_label) = primary_span_label {
254 write!(&mut message, "\n{}", primary_span_label).unwrap();
255 }
256
257 if is_unused_or_unnecessary(rd) {
258 tags.push(DiagnosticTag::Unnecessary);
259 }
260
261 if is_deprecated(rd) {
262 tags.push(DiagnosticTag::Deprecated);
263 }
264
265 let diagnostic = Diagnostic {
266 range: location.range,
267 severity,
268 code: code.map(NumberOrString::String),
269 source: Some(source),
270 message,
271 related_information: if !related_information.is_empty() {
272 Some(related_information)
273 } else {
274 None
275 },
276 tags: if !tags.is_empty() { Some(tags) } else { None },
277 };
278
279 Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes })
280}
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_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..69a07be11
--- /dev/null
+++ b/crates/ra_cargo_watch/src/conv/test.rs
@@ -0,0 +1,698 @@
1use crate::*;
2
3fn parse_diagnostic(val: &str) -> cargo_metadata::diagnostic::Diagnostic {
4 serde_json::from_str::<cargo_metadata::diagnostic::Diagnostic>(val).unwrap()
5}
6
7#[test]
8fn snap_rustc_incompatible_type_for_trait() {
9 let diag = parse_diagnostic(
10 r##"{
11 "message": "method `next` has an incompatible type for trait",
12 "code": {
13 "code": "E0053",
14 "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"
15 },
16 "level": "error",
17 "spans": [
18 {
19 "file_name": "compiler/ty/list_iter.rs",
20 "byte_start": 1307,
21 "byte_end": 1350,
22 "line_start": 52,
23 "line_end": 52,
24 "column_start": 5,
25 "column_end": 48,
26 "is_primary": true,
27 "text": [
28 {
29 "text": " fn next(&self) -> Option<&'list ty::Ref<M>> {",
30 "highlight_start": 5,
31 "highlight_end": 48
32 }
33 ],
34 "label": "types differ in mutability",
35 "suggested_replacement": null,
36 "suggestion_applicability": null,
37 "expansion": null
38 }
39 ],
40 "children": [
41 {
42 "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>>`",
43 "code": null,
44 "level": "note",
45 "spans": [],
46 "children": [],
47 "rendered": null
48 }
49 ],
50 "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"
51 }
52 "##,
53 );
54
55 let workspace_root = PathBuf::from("/test/");
56 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
57 insta::assert_debug_snapshot!(diag);
58}
59
60#[test]
61fn snap_rustc_unused_variable() {
62 let diag = parse_diagnostic(
63 r##"{
64"message": "unused variable: `foo`",
65"code": {
66 "code": "unused_variables",
67 "explanation": null
68},
69"level": "warning",
70"spans": [
71 {
72 "file_name": "driver/subcommand/repl.rs",
73 "byte_start": 9228,
74 "byte_end": 9231,
75 "line_start": 291,
76 "line_end": 291,
77 "column_start": 9,
78 "column_end": 12,
79 "is_primary": true,
80 "text": [
81 {
82 "text": " let foo = 42;",
83 "highlight_start": 9,
84 "highlight_end": 12
85 }
86 ],
87 "label": null,
88 "suggested_replacement": null,
89 "suggestion_applicability": null,
90 "expansion": null
91 }
92],
93"children": [
94 {
95 "message": "#[warn(unused_variables)] on by default",
96 "code": null,
97 "level": "note",
98 "spans": [],
99 "children": [],
100 "rendered": null
101 },
102 {
103 "message": "consider prefixing with an underscore",
104 "code": null,
105 "level": "help",
106 "spans": [
107 {
108 "file_name": "driver/subcommand/repl.rs",
109 "byte_start": 9228,
110 "byte_end": 9231,
111 "line_start": 291,
112 "line_end": 291,
113 "column_start": 9,
114 "column_end": 12,
115 "is_primary": true,
116 "text": [
117 {
118 "text": " let foo = 42;",
119 "highlight_start": 9,
120 "highlight_end": 12
121 }
122 ],
123 "label": null,
124 "suggested_replacement": "_foo",
125 "suggestion_applicability": "MachineApplicable",
126 "expansion": null
127 }
128 ],
129 "children": [],
130 "rendered": null
131 }
132],
133"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"
134}"##,
135 );
136
137 let workspace_root = PathBuf::from("/test/");
138 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
139 insta::assert_debug_snapshot!(diag);
140}
141
142#[test]
143fn snap_rustc_wrong_number_of_parameters() {
144 let diag = parse_diagnostic(
145 r##"{
146"message": "this function takes 2 parameters but 3 parameters were supplied",
147"code": {
148 "code": "E0061",
149 "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"
150},
151"level": "error",
152"spans": [
153 {
154 "file_name": "compiler/ty/select.rs",
155 "byte_start": 8787,
156 "byte_end": 9241,
157 "line_start": 219,
158 "line_end": 231,
159 "column_start": 5,
160 "column_end": 6,
161 "is_primary": false,
162 "text": [
163 {
164 "text": " pub fn add_evidence(",
165 "highlight_start": 5,
166 "highlight_end": 25
167 },
168 {
169 "text": " &mut self,",
170 "highlight_start": 1,
171 "highlight_end": 19
172 },
173 {
174 "text": " target_poly: &ty::Ref<ty::Poly>,",
175 "highlight_start": 1,
176 "highlight_end": 41
177 },
178 {
179 "text": " evidence_poly: &ty::Ref<ty::Poly>,",
180 "highlight_start": 1,
181 "highlight_end": 43
182 },
183 {
184 "text": " ) {",
185 "highlight_start": 1,
186 "highlight_end": 8
187 },
188 {
189 "text": " match target_poly {",
190 "highlight_start": 1,
191 "highlight_end": 28
192 },
193 {
194 "text": " ty::Ref::Var(tvar, _) => self.add_var_evidence(tvar, evidence_poly),",
195 "highlight_start": 1,
196 "highlight_end": 81
197 },
198 {
199 "text": " ty::Ref::Fixed(target_ty) => {",
200 "highlight_start": 1,
201 "highlight_end": 43
202 },
203 {
204 "text": " let evidence_ty = evidence_poly.resolve_to_ty();",
205 "highlight_start": 1,
206 "highlight_end": 65
207 },
208 {
209 "text": " self.add_evidence_ty(target_ty, evidence_poly, evidence_ty)",
210 "highlight_start": 1,
211 "highlight_end": 76
212 },
213 {
214 "text": " }",
215 "highlight_start": 1,
216 "highlight_end": 14
217 },
218 {
219 "text": " }",
220 "highlight_start": 1,
221 "highlight_end": 10
222 },
223 {
224 "text": " }",
225 "highlight_start": 1,
226 "highlight_end": 6
227 }
228 ],
229 "label": "defined here",
230 "suggested_replacement": null,
231 "suggestion_applicability": null,
232 "expansion": null
233 },
234 {
235 "file_name": "compiler/ty/select.rs",
236 "byte_start": 4045,
237 "byte_end": 4057,
238 "line_start": 104,
239 "line_end": 104,
240 "column_start": 18,
241 "column_end": 30,
242 "is_primary": true,
243 "text": [
244 {
245 "text": " self.add_evidence(target_fixed, evidence_fixed, false);",
246 "highlight_start": 18,
247 "highlight_end": 30
248 }
249 ],
250 "label": "expected 2 parameters",
251 "suggested_replacement": null,
252 "suggestion_applicability": null,
253 "expansion": null
254 }
255],
256"children": [],
257"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"
258}"##,
259 );
260
261 let workspace_root = PathBuf::from("/test/");
262 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
263 insta::assert_debug_snapshot!(diag);
264}
265
266#[test]
267fn snap_clippy_pass_by_ref() {
268 let diag = parse_diagnostic(
269 r##"{
270"message": "this argument is passed by reference, but would be more efficient if passed by value",
271"code": {
272 "code": "clippy::trivially_copy_pass_by_ref",
273 "explanation": null
274},
275"level": "warning",
276"spans": [
277 {
278 "file_name": "compiler/mir/tagset.rs",
279 "byte_start": 941,
280 "byte_end": 946,
281 "line_start": 42,
282 "line_end": 42,
283 "column_start": 24,
284 "column_end": 29,
285 "is_primary": true,
286 "text": [
287 {
288 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
289 "highlight_start": 24,
290 "highlight_end": 29
291 }
292 ],
293 "label": null,
294 "suggested_replacement": null,
295 "suggestion_applicability": null,
296 "expansion": null
297 }
298],
299"children": [
300 {
301 "message": "lint level defined here",
302 "code": null,
303 "level": "note",
304 "spans": [
305 {
306 "file_name": "compiler/lib.rs",
307 "byte_start": 8,
308 "byte_end": 19,
309 "line_start": 1,
310 "line_end": 1,
311 "column_start": 9,
312 "column_end": 20,
313 "is_primary": true,
314 "text": [
315 {
316 "text": "#![warn(clippy::all)]",
317 "highlight_start": 9,
318 "highlight_end": 20
319 }
320 ],
321 "label": null,
322 "suggested_replacement": null,
323 "suggestion_applicability": null,
324 "expansion": null
325 }
326 ],
327 "children": [],
328 "rendered": null
329 },
330 {
331 "message": "#[warn(clippy::trivially_copy_pass_by_ref)] implied by #[warn(clippy::all)]",
332 "code": null,
333 "level": "note",
334 "spans": [],
335 "children": [],
336 "rendered": null
337 },
338 {
339 "message": "for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#trivially_copy_pass_by_ref",
340 "code": null,
341 "level": "help",
342 "spans": [],
343 "children": [],
344 "rendered": null
345 },
346 {
347 "message": "consider passing by value instead",
348 "code": null,
349 "level": "help",
350 "spans": [
351 {
352 "file_name": "compiler/mir/tagset.rs",
353 "byte_start": 941,
354 "byte_end": 946,
355 "line_start": 42,
356 "line_end": 42,
357 "column_start": 24,
358 "column_end": 29,
359 "is_primary": true,
360 "text": [
361 {
362 "text": " pub fn is_disjoint(&self, other: Self) -> bool {",
363 "highlight_start": 24,
364 "highlight_end": 29
365 }
366 ],
367 "label": null,
368 "suggested_replacement": "self",
369 "suggestion_applicability": "Unspecified",
370 "expansion": null
371 }
372 ],
373 "children": [],
374 "rendered": null
375 }
376],
377"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"
378}"##,
379 );
380
381 let workspace_root = PathBuf::from("/test/");
382 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
383 insta::assert_debug_snapshot!(diag);
384}
385
386#[test]
387fn snap_rustc_mismatched_type() {
388 let diag = parse_diagnostic(
389 r##"{
390"message": "mismatched types",
391"code": {
392 "code": "E0308",
393 "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"
394},
395"level": "error",
396"spans": [
397 {
398 "file_name": "runtime/compiler_support.rs",
399 "byte_start": 1589,
400 "byte_end": 1594,
401 "line_start": 48,
402 "line_end": 48,
403 "column_start": 65,
404 "column_end": 70,
405 "is_primary": true,
406 "text": [
407 {
408 "text": " let layout = alloc::Layout::from_size_align_unchecked(size, align);",
409 "highlight_start": 65,
410 "highlight_end": 70
411 }
412 ],
413 "label": "expected usize, found u32",
414 "suggested_replacement": null,
415 "suggestion_applicability": null,
416 "expansion": null
417 }
418],
419"children": [],
420"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"
421}"##,
422 );
423
424 let workspace_root = PathBuf::from("/test/");
425 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
426 insta::assert_debug_snapshot!(diag);
427}
428
429#[test]
430fn snap_handles_macro_location() {
431 let diag = parse_diagnostic(
432 r##"{
433"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",
434"children": [
435 {
436 "children": [],
437 "code": null,
438 "level": "help",
439 "message": "the trait `std::cmp::PartialEq<&str>` is not implemented for `{integer}`",
440 "rendered": null,
441 "spans": []
442 }
443],
444"code": {
445 "code": "E0277",
446 "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"
447},
448"level": "error",
449"message": "can't compare `{integer}` with `&str`",
450"spans": [
451 {
452 "byte_end": 155,
453 "byte_start": 153,
454 "column_end": 33,
455 "column_start": 31,
456 "expansion": {
457 "def_site_span": {
458 "byte_end": 940,
459 "byte_start": 0,
460 "column_end": 6,
461 "column_start": 1,
462 "expansion": null,
463 "file_name": "<::core::macros::assert_eq macros>",
464 "is_primary": false,
465 "label": null,
466 "line_end": 36,
467 "line_start": 1,
468 "suggested_replacement": null,
469 "suggestion_applicability": null,
470 "text": [
471 {
472 "highlight_end": 35,
473 "highlight_start": 1,
474 "text": "($ left : expr, $ right : expr) =>"
475 },
476 {
477 "highlight_end": 3,
478 "highlight_start": 1,
479 "text": "({"
480 },
481 {
482 "highlight_end": 33,
483 "highlight_start": 1,
484 "text": " match (& $ left, & $ right)"
485 },
486 {
487 "highlight_end": 7,
488 "highlight_start": 1,
489 "text": " {"
490 },
491 {
492 "highlight_end": 34,
493 "highlight_start": 1,
494 "text": " (left_val, right_val) =>"
495 },
496 {
497 "highlight_end": 11,
498 "highlight_start": 1,
499 "text": " {"
500 },
501 {
502 "highlight_end": 46,
503 "highlight_start": 1,
504 "text": " if ! (* left_val == * right_val)"
505 },
506 {
507 "highlight_end": 15,
508 "highlight_start": 1,
509 "text": " {"
510 },
511 {
512 "highlight_end": 25,
513 "highlight_start": 1,
514 "text": " panic !"
515 },
516 {
517 "highlight_end": 57,
518 "highlight_start": 1,
519 "text": " (r#\"assertion failed: `(left == right)`"
520 },
521 {
522 "highlight_end": 16,
523 "highlight_start": 1,
524 "text": " left: `{:?}`,"
525 },
526 {
527 "highlight_end": 18,
528 "highlight_start": 1,
529 "text": " right: `{:?}`\"#,"
530 },
531 {
532 "highlight_end": 47,
533 "highlight_start": 1,
534 "text": " & * left_val, & * right_val)"
535 },
536 {
537 "highlight_end": 15,
538 "highlight_start": 1,
539 "text": " }"
540 },
541 {
542 "highlight_end": 11,
543 "highlight_start": 1,
544 "text": " }"
545 },
546 {
547 "highlight_end": 7,
548 "highlight_start": 1,
549 "text": " }"
550 },
551 {
552 "highlight_end": 42,
553 "highlight_start": 1,
554 "text": " }) ; ($ left : expr, $ right : expr,) =>"
555 },
556 {
557 "highlight_end": 49,
558 "highlight_start": 1,
559 "text": "({ $ crate :: assert_eq ! ($ left, $ right) }) ;"
560 },
561 {
562 "highlight_end": 53,
563 "highlight_start": 1,
564 "text": "($ left : expr, $ right : expr, $ ($ arg : tt) +) =>"
565 },
566 {
567 "highlight_end": 3,
568 "highlight_start": 1,
569 "text": "({"
570 },
571 {
572 "highlight_end": 37,
573 "highlight_start": 1,
574 "text": " match (& ($ left), & ($ right))"
575 },
576 {
577 "highlight_end": 7,
578 "highlight_start": 1,
579 "text": " {"
580 },
581 {
582 "highlight_end": 34,
583 "highlight_start": 1,
584 "text": " (left_val, right_val) =>"
585 },
586 {
587 "highlight_end": 11,
588 "highlight_start": 1,
589 "text": " {"
590 },
591 {
592 "highlight_end": 46,
593 "highlight_start": 1,
594 "text": " if ! (* left_val == * right_val)"
595 },
596 {
597 "highlight_end": 15,
598 "highlight_start": 1,
599 "text": " {"
600 },
601 {
602 "highlight_end": 25,
603 "highlight_start": 1,
604 "text": " panic !"
605 },
606 {
607 "highlight_end": 57,
608 "highlight_start": 1,
609 "text": " (r#\"assertion failed: `(left == right)`"
610 },
611 {
612 "highlight_end": 16,
613 "highlight_start": 1,
614 "text": " left: `{:?}`,"
615 },
616 {
617 "highlight_end": 22,
618 "highlight_start": 1,
619 "text": " right: `{:?}`: {}\"#,"
620 },
621 {
622 "highlight_end": 72,
623 "highlight_start": 1,
624 "text": " & * left_val, & * right_val, $ crate :: format_args !"
625 },
626 {
627 "highlight_end": 33,
628 "highlight_start": 1,
629 "text": " ($ ($ arg) +))"
630 },
631 {
632 "highlight_end": 15,
633 "highlight_start": 1,
634 "text": " }"
635 },
636 {
637 "highlight_end": 11,
638 "highlight_start": 1,
639 "text": " }"
640 },
641 {
642 "highlight_end": 7,
643 "highlight_start": 1,
644 "text": " }"
645 },
646 {
647 "highlight_end": 6,
648 "highlight_start": 1,
649 "text": " }) ;"
650 }
651 ]
652 },
653 "macro_decl_name": "assert_eq!",
654 "span": {
655 "byte_end": 38,
656 "byte_start": 16,
657 "column_end": 27,
658 "column_start": 5,
659 "expansion": null,
660 "file_name": "src/main.rs",
661 "is_primary": false,
662 "label": null,
663 "line_end": 2,
664 "line_start": 2,
665 "suggested_replacement": null,
666 "suggestion_applicability": null,
667 "text": [
668 {
669 "highlight_end": 27,
670 "highlight_start": 5,
671 "text": " assert_eq!(1, \"love\");"
672 }
673 ]
674 }
675 },
676 "file_name": "<::core::macros::assert_eq macros>",
677 "is_primary": true,
678 "label": "no implementation for `{integer} == &str`",
679 "line_end": 7,
680 "line_start": 7,
681 "suggested_replacement": null,
682 "suggestion_applicability": null,
683 "text": [
684 {
685 "highlight_end": 33,
686 "highlight_start": 31,
687 "text": " if ! (* left_val == * right_val)"
688 }
689 ]
690 }
691]
692}"##,
693 );
694
695 let workspace_root = PathBuf::from("/test/");
696 let diag = map_rust_diagnostic_to_lsp(&diag, &workspace_root).expect("couldn't map diagnostic");
697 insta::assert_debug_snapshot!(diag);
698}
diff --git a/crates/ra_cargo_watch/src/lib.rs b/crates/ra_cargo_watch/src/lib.rs
new file mode 100644
index 000000000..c86386610
--- /dev/null
+++ b/crates/ra_cargo_watch/src/lib.rs
@@ -0,0 +1,345 @@
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::{select, unbounded, Receiver, RecvError, Sender, TryRecvError};
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
24#[derive(Clone, Debug)]
25pub struct CheckOptions {
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#[derive(Debug)]
36pub struct CheckWatcher {
37 pub task_recv: Receiver<CheckTask>,
38 pub cmd_send: Sender<CheckCommand>,
39 pub shared: Arc<RwLock<CheckWatcherSharedState>>,
40 handle: JoinHandle<()>,
41}
42
43impl CheckWatcher {
44 pub fn new(options: &CheckOptions, workspace_root: PathBuf) -> CheckWatcher {
45 let options = options.clone();
46 let shared = Arc::new(RwLock::new(CheckWatcherSharedState::new()));
47
48 let (task_send, task_recv) = unbounded::<CheckTask>();
49 let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
50 let shared_ = shared.clone();
51 let handle = std::thread::spawn(move || {
52 let mut check = CheckWatcherState::new(options, workspace_root, shared_);
53 check.run(&task_send, &cmd_recv);
54 });
55
56 CheckWatcher { task_recv, cmd_send, handle, shared }
57 }
58
59 /// Schedule a re-start of the cargo check worker.
60 pub fn update(&self) {
61 self.cmd_send.send(CheckCommand::Update).unwrap();
62 }
63}
64
65pub struct CheckWatcherState {
66 options: CheckOptions,
67 workspace_root: PathBuf,
68 running: bool,
69 watcher: WatchThread,
70 last_update_req: Option<Instant>,
71 shared: Arc<RwLock<CheckWatcherSharedState>>,
72}
73
74#[derive(Debug)]
75pub struct CheckWatcherSharedState {
76 diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
77 suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
78}
79
80impl CheckWatcherSharedState {
81 fn new() -> CheckWatcherSharedState {
82 CheckWatcherSharedState {
83 diagnostic_collection: HashMap::new(),
84 suggested_fix_collection: HashMap::new(),
85 }
86 }
87
88 /// Clear the cached diagnostics, and schedule updating diagnostics by the
89 /// server, to clear stale results.
90 pub fn clear(&mut self, task_send: &Sender<CheckTask>) {
91 let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect();
92
93 self.diagnostic_collection.clear();
94 self.suggested_fix_collection.clear();
95
96 for uri in cleared_files {
97 task_send.send(CheckTask::Update(uri.clone())).unwrap();
98 }
99 }
100
101 pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> {
102 self.diagnostic_collection.get(uri).map(|d| d.as_slice())
103 }
104
105 pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> {
106 self.suggested_fix_collection.get(uri).map(|d| d.as_slice())
107 }
108
109 fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) {
110 let diagnostics = self.diagnostic_collection.entry(file_uri).or_default();
111
112 // If we're building multiple targets it's possible we've already seen this diagnostic
113 let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic));
114 if is_duplicate {
115 return;
116 }
117
118 diagnostics.push(diagnostic);
119 }
120
121 fn add_suggested_fix_for_diagnostic(
122 &mut self,
123 mut suggested_fix: SuggestedFix,
124 diagnostic: &Diagnostic,
125 ) {
126 let file_uri = suggested_fix.location.uri.clone();
127 let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default();
128
129 let existing_suggestion: Option<&mut SuggestedFix> =
130 file_suggestions.iter_mut().find(|s| s == &&suggested_fix);
131 if let Some(existing_suggestion) = existing_suggestion {
132 // The existing suggestion also applies to this new diagnostic
133 existing_suggestion.diagnostics.push(diagnostic.clone());
134 } else {
135 // We haven't seen this suggestion before
136 suggested_fix.diagnostics.push(diagnostic.clone());
137 file_suggestions.push(suggested_fix);
138 }
139 }
140}
141
142#[derive(Debug)]
143pub enum CheckTask {
144 /// Request a update of the given files diagnostics
145 Update(Url),
146
147 /// Request check progress notification to client
148 Status(WorkDoneProgress),
149}
150
151pub enum CheckCommand {
152 /// Request re-start of check thread
153 Update,
154}
155
156impl CheckWatcherState {
157 pub fn new(
158 options: CheckOptions,
159 workspace_root: PathBuf,
160 shared: Arc<RwLock<CheckWatcherSharedState>>,
161 ) -> CheckWatcherState {
162 let watcher = WatchThread::new(&options, &workspace_root);
163 CheckWatcherState {
164 options,
165 workspace_root,
166 running: false,
167 watcher,
168 last_update_req: None,
169 shared,
170 }
171 }
172
173 pub fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
174 self.running = true;
175 while self.running {
176 select! {
177 recv(&cmd_recv) -> cmd => match cmd {
178 Ok(cmd) => self.handle_command(cmd),
179 Err(RecvError) => {
180 // Command channel has closed, so shut down
181 self.running = false;
182 },
183 },
184 recv(self.watcher.message_recv) -> msg => match msg {
185 Ok(msg) => self.handle_message(msg, task_send),
186 Err(RecvError) => {},
187 }
188 };
189
190 if self.should_recheck() {
191 self.last_update_req.take();
192 self.shared.write().clear(task_send);
193
194 self.watcher.cancel();
195 self.watcher = WatchThread::new(&self.options, &self.workspace_root);
196 }
197 }
198 }
199
200 fn should_recheck(&mut self) -> bool {
201 if let Some(_last_update_req) = &self.last_update_req {
202 // We currently only request an update on save, as we need up to
203 // date source on disk for cargo check to do it's magic, so we
204 // don't really need to debounce the requests at this point.
205 return true;
206 }
207 false
208 }
209
210 fn handle_command(&mut self, cmd: CheckCommand) {
211 match cmd {
212 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
213 }
214 }
215
216 fn handle_message(&mut self, msg: CheckEvent, task_send: &Sender<CheckTask>) {
217 match msg {
218 CheckEvent::Begin => {
219 task_send
220 .send(CheckTask::Status(WorkDoneProgress::Begin(WorkDoneProgressBegin {
221 title: "Running 'cargo check'".to_string(),
222 cancellable: Some(false),
223 message: None,
224 percentage: None,
225 })))
226 .unwrap();
227 }
228
229 CheckEvent::End => {
230 task_send
231 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd {
232 message: None,
233 })))
234 .unwrap();
235 }
236
237 CheckEvent::Msg(Message::CompilerArtifact(msg)) => {
238 task_send
239 .send(CheckTask::Status(WorkDoneProgress::Report(WorkDoneProgressReport {
240 cancellable: Some(false),
241 message: Some(msg.target.name),
242 percentage: None,
243 })))
244 .unwrap();
245 }
246
247 CheckEvent::Msg(Message::CompilerMessage(msg)) => {
248 let map_result =
249 match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) {
250 Some(map_result) => map_result,
251 None => return,
252 };
253
254 let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result;
255 let file_uri = location.uri.clone();
256
257 if !suggested_fixes.is_empty() {
258 for suggested_fix in suggested_fixes {
259 self.shared
260 .write()
261 .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic);
262 }
263 }
264 self.shared.write().add_diagnostic(file_uri, diagnostic);
265
266 task_send.send(CheckTask::Update(location.uri)).unwrap();
267 }
268
269 CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
270 CheckEvent::Msg(Message::Unknown) => {}
271 }
272 }
273}
274
275/// WatchThread exists to wrap around the communication needed to be able to
276/// run `cargo check` without blocking. Currently the Rust standard library
277/// doesn't provide a way to read sub-process output without blocking, so we
278/// have to wrap sub-processes output handling in a thread and pass messages
279/// back over a channel.
280struct WatchThread {
281 message_recv: Receiver<CheckEvent>,
282 cancel_send: Sender<()>,
283}
284
285enum CheckEvent {
286 Begin,
287 Msg(cargo_metadata::Message),
288 End,
289}
290
291impl WatchThread {
292 fn new(options: &CheckOptions, workspace_root: &PathBuf) -> WatchThread {
293 let mut args: Vec<String> = vec![
294 options.command.clone(),
295 "--message-format=json".to_string(),
296 "--manifest-path".to_string(),
297 format!("{}/Cargo.toml", workspace_root.to_string_lossy()),
298 ];
299 if options.all_targets {
300 args.push("--all-targets".to_string());
301 }
302 args.extend(options.args.iter().cloned());
303
304 let (message_send, message_recv) = unbounded();
305 let (cancel_send, cancel_recv) = unbounded();
306 let enabled = options.enable;
307 std::thread::spawn(move || {
308 if !enabled {
309 return;
310 }
311
312 let mut command = Command::new("cargo")
313 .args(&args)
314 .stdout(Stdio::piped())
315 .stderr(Stdio::null())
316 .spawn()
317 .expect("couldn't launch cargo");
318
319 message_send.send(CheckEvent::Begin).unwrap();
320 for message in cargo_metadata::parse_messages(command.stdout.take().unwrap()) {
321 match cancel_recv.try_recv() {
322 Ok(()) | Err(TryRecvError::Disconnected) => {
323 command.kill().expect("couldn't kill command");
324 }
325 Err(TryRecvError::Empty) => (),
326 }
327
328 message_send.send(CheckEvent::Msg(message.unwrap())).unwrap();
329 }
330 message_send.send(CheckEvent::End).unwrap();
331 });
332 WatchThread { message_recv, cancel_send }
333 }
334
335 fn cancel(&self) {
336 let _ = self.cancel_send.send(());
337 }
338}
339
340fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
341 left.source == right.source
342 && left.severity == right.severity
343 && left.range == right.range
344 && left.message == right.message
345}