aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_flycheck/src/lib.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_flycheck/src/lib.rs')
-rw-r--r--crates/ra_flycheck/src/lib.rs359
1 files changed, 359 insertions, 0 deletions
diff --git a/crates/ra_flycheck/src/lib.rs b/crates/ra_flycheck/src/lib.rs
new file mode 100644
index 000000000..75aece45f
--- /dev/null
+++ b/crates/ra_flycheck/src/lib.rs
@@ -0,0 +1,359 @@
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.
4mod conv;
5
6use std::{
7 error, fmt,
8 io::{BufRead, BufReader},
9 path::{Path, PathBuf},
10 process::{Command, Stdio},
11 time::Instant,
12};
13
14use cargo_metadata::Message;
15use crossbeam_channel::{never, select, unbounded, Receiver, RecvError, Sender};
16use lsp_types::{
17 CodeAction, CodeActionOrCommand, Diagnostic, Url, WorkDoneProgress, WorkDoneProgressBegin,
18 WorkDoneProgressEnd, WorkDoneProgressReport,
19};
20
21use crate::conv::{map_rust_diagnostic_to_lsp, MappedRustDiagnostic};
22
23pub use crate::conv::url_from_path_with_drive_lowercasing;
24
25#[derive(Clone, Debug)]
26pub struct CheckConfig {
27 pub args: Vec<String>,
28 pub command: String,
29 pub all_targets: bool,
30}
31
32/// Flycheck 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 Flycheck {
38 // XXX: drop order is significant
39 cmd_send: Sender<CheckCommand>,
40 handle: jod_thread::JoinHandle<()>,
41 pub task_recv: Receiver<CheckTask>,
42}
43
44impl Flycheck {
45 pub fn new(config: CheckConfig, workspace_root: PathBuf) -> Flycheck {
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 = FlycheckThread::new(config, workspace_root);
50 check.run(&task_send, &cmd_recv);
51 });
52 Flycheck { task_recv, cmd_send, 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 FlycheckThread {
79 options: CheckConfig,
80 workspace_root: PathBuf,
81 last_update_req: Option<Instant>,
82 // XXX: drop order is significant
83 message_recv: Receiver<CheckEvent>,
84 /// WatchThread exists to wrap around the communication needed to be able to
85 /// run `cargo check` without blocking. Currently the Rust standard library
86 /// doesn't provide a way to read sub-process output without blocking, so we
87 /// have to wrap sub-processes output handling in a thread and pass messages
88 /// back over a channel.
89 check_process: Option<jod_thread::JoinHandle<()>>,
90}
91
92impl FlycheckThread {
93 fn new(options: CheckConfig, workspace_root: PathBuf) -> FlycheckThread {
94 FlycheckThread {
95 options,
96 workspace_root,
97 last_update_req: None,
98 message_recv: never(),
99 check_process: None,
100 }
101 }
102
103 fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
104 // If we rerun the thread, we need to discard the previous check results first
105 self.clean_previous_results(task_send);
106
107 loop {
108 select! {
109 recv(&cmd_recv) -> cmd => match cmd {
110 Ok(cmd) => self.handle_command(cmd),
111 Err(RecvError) => {
112 // Command channel has closed, so shut down
113 break;
114 },
115 },
116 recv(self.message_recv) -> msg => match msg {
117 Ok(msg) => self.handle_message(msg, task_send),
118 Err(RecvError) => {
119 // Watcher finished, replace it with a never channel to
120 // avoid busy-waiting.
121 self.message_recv = never();
122 self.check_process = None;
123 },
124 }
125 };
126
127 if self.should_recheck() {
128 self.last_update_req = None;
129 task_send.send(CheckTask::ClearDiagnostics).unwrap();
130 self.restart_check_process();
131 }
132 }
133 }
134
135 fn clean_previous_results(&self, task_send: &Sender<CheckTask>) {
136 task_send.send(CheckTask::ClearDiagnostics).unwrap();
137 task_send
138 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd { message: None })))
139 .unwrap();
140 }
141
142 fn should_recheck(&mut self) -> bool {
143 if let Some(_last_update_req) = &self.last_update_req {
144 // We currently only request an update on save, as we need up to
145 // date source on disk for cargo check to do it's magic, so we
146 // don't really need to debounce the requests at this point.
147 return true;
148 }
149 false
150 }
151
152 fn handle_command(&mut self, cmd: CheckCommand) {
153 match cmd {
154 CheckCommand::Update => self.last_update_req = Some(Instant::now()),
155 }
156 }
157
158 fn handle_message(&self, msg: CheckEvent, task_send: &Sender<CheckTask>) {
159 match msg {
160 CheckEvent::Begin => {
161 task_send
162 .send(CheckTask::Status(WorkDoneProgress::Begin(WorkDoneProgressBegin {
163 title: "Running 'cargo check'".to_string(),
164 cancellable: Some(false),
165 message: None,
166 percentage: None,
167 })))
168 .unwrap();
169 }
170
171 CheckEvent::End => {
172 task_send
173 .send(CheckTask::Status(WorkDoneProgress::End(WorkDoneProgressEnd {
174 message: None,
175 })))
176 .unwrap();
177 }
178
179 CheckEvent::Msg(Message::CompilerArtifact(msg)) => {
180 task_send
181 .send(CheckTask::Status(WorkDoneProgress::Report(WorkDoneProgressReport {
182 cancellable: Some(false),
183 message: Some(msg.target.name),
184 percentage: None,
185 })))
186 .unwrap();
187 }
188
189 CheckEvent::Msg(Message::CompilerMessage(msg)) => {
190 let map_result = map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root);
191 if map_result.is_empty() {
192 return;
193 }
194
195 for MappedRustDiagnostic { location, diagnostic, fixes } in map_result {
196 let fixes = fixes
197 .into_iter()
198 .map(|fix| {
199 CodeAction { diagnostics: Some(vec![diagnostic.clone()]), ..fix }.into()
200 })
201 .collect();
202
203 task_send
204 .send(CheckTask::AddDiagnostic { url: location.uri, diagnostic, fixes })
205 .unwrap();
206 }
207 }
208
209 CheckEvent::Msg(Message::BuildScriptExecuted(_msg)) => {}
210 CheckEvent::Msg(Message::Unknown) => {}
211 }
212 }
213
214 fn restart_check_process(&mut self) {
215 // First, clear and cancel the old thread
216 self.message_recv = never();
217 self.check_process = None;
218
219 let mut args: Vec<String> = vec![
220 self.options.command.clone(),
221 "--workspace".to_string(),
222 "--message-format=json".to_string(),
223 "--manifest-path".to_string(),
224 format!("{}/Cargo.toml", self.workspace_root.display()),
225 ];
226 if self.options.all_targets {
227 args.push("--all-targets".to_string());
228 }
229 args.extend(self.options.args.iter().cloned());
230
231 let (message_send, message_recv) = unbounded();
232 let workspace_root = self.workspace_root.to_owned();
233 self.message_recv = message_recv;
234 self.check_process = Some(jod_thread::spawn(move || {
235 // If we trigger an error here, we will do so in the loop instead,
236 // which will break out of the loop, and continue the shutdown
237 let _ = message_send.send(CheckEvent::Begin);
238
239 let res = run_cargo(&args, Some(&workspace_root), &mut |message| {
240 // Skip certain kinds of messages to only spend time on what's useful
241 match &message {
242 Message::CompilerArtifact(artifact) if artifact.fresh => return true,
243 Message::BuildScriptExecuted(_) => return true,
244 Message::Unknown => return true,
245 _ => {}
246 }
247
248 // if the send channel was closed, we want to shutdown
249 message_send.send(CheckEvent::Msg(message)).is_ok()
250 });
251
252 if let Err(err) = res {
253 // FIXME: make the `message_send` to be `Sender<Result<CheckEvent, CargoError>>`
254 // to display user-caused misconfiguration errors instead of just logging them here
255 log::error!("Cargo watcher failed {:?}", err);
256 }
257
258 // We can ignore any error here, as we are already in the progress
259 // of shutting down.
260 let _ = message_send.send(CheckEvent::End);
261 }))
262 }
263}
264
265#[derive(Debug)]
266pub struct DiagnosticWithFixes {
267 diagnostic: Diagnostic,
268 fixes: Vec<CodeAction>,
269}
270
271enum CheckEvent {
272 Begin,
273 Msg(cargo_metadata::Message),
274 End,
275}
276
277#[derive(Debug)]
278pub struct CargoError(String);
279
280impl fmt::Display for CargoError {
281 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282 write!(f, "Cargo failed: {}", self.0)
283 }
284}
285impl error::Error for CargoError {}
286
287fn run_cargo(
288 args: &[String],
289 current_dir: Option<&Path>,
290 on_message: &mut dyn FnMut(cargo_metadata::Message) -> bool,
291) -> Result<(), CargoError> {
292 let mut command = Command::new("cargo");
293 if let Some(current_dir) = current_dir {
294 command.current_dir(current_dir);
295 }
296
297 let mut child = command
298 .args(args)
299 .stdout(Stdio::piped())
300 .stderr(Stdio::null())
301 .stdin(Stdio::null())
302 .spawn()
303 .expect("couldn't launch cargo");
304
305 // We manually read a line at a time, instead of using serde's
306 // stream deserializers, because the deserializer cannot recover
307 // from an error, resulting in it getting stuck, because we try to
308 // be resillient against failures.
309 //
310 // Because cargo only outputs one JSON object per line, we can
311 // simply skip a line if it doesn't parse, which just ignores any
312 // erroneus output.
313 let stdout = BufReader::new(child.stdout.take().unwrap());
314 let mut read_at_least_one_message = false;
315
316 for line in stdout.lines() {
317 let line = match line {
318 Ok(line) => line,
319 Err(err) => {
320 log::error!("Couldn't read line from cargo: {}", err);
321 continue;
322 }
323 };
324
325 let message = serde_json::from_str::<cargo_metadata::Message>(&line);
326 let message = match message {
327 Ok(message) => message,
328 Err(err) => {
329 log::error!("Invalid json from cargo check, ignoring ({}): {:?} ", err, line);
330 continue;
331 }
332 };
333
334 read_at_least_one_message = true;
335
336 if !on_message(message) {
337 break;
338 }
339 }
340
341 // It is okay to ignore the result, as it only errors if the process is already dead
342 let _ = child.kill();
343
344 let err_msg = match child.wait() {
345 Ok(exit_code) if !exit_code.success() && !read_at_least_one_message => {
346 // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
347 // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
348 format!(
349 "the command produced no valid metadata (exit code: {:?}): cargo {}",
350 exit_code,
351 args.join(" ")
352 )
353 }
354 Err(err) => format!("io error: {:?}", err),
355 Ok(_) => return Ok(()),
356 };
357
358 Err(CargoError(err_msg))
359}