]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/flycheck/src/lib.rs
:arrow_up: rust-analyzer
[rust.git] / src / tools / rust-analyzer / crates / flycheck / src / lib.rs
1 //! Flycheck 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.
4
5 #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
6
7 use std::{
8     fmt, io,
9     process::{ChildStderr, ChildStdout, Command, Stdio},
10     time::Duration,
11 };
12
13 use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
14 use paths::AbsPathBuf;
15 use rustc_hash::FxHashMap;
16 use serde::Deserialize;
17 use stdx::{process::streaming_output, JodChild};
18
19 pub use cargo_metadata::diagnostic::{
20     Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
21     DiagnosticSpanMacroExpansion,
22 };
23
24 #[derive(Clone, Debug, PartialEq, Eq)]
25 pub enum FlycheckConfig {
26     CargoCommand {
27         command: String,
28         target_triple: Option<String>,
29         all_targets: bool,
30         no_default_features: bool,
31         all_features: bool,
32         features: Vec<String>,
33         extra_args: Vec<String>,
34         extra_env: FxHashMap<String, String>,
35     },
36     CustomCommand {
37         command: String,
38         args: Vec<String>,
39         extra_env: FxHashMap<String, String>,
40     },
41 }
42
43 impl fmt::Display for FlycheckConfig {
44     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45         match self {
46             FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
47             FlycheckConfig::CustomCommand { command, args, .. } => {
48                 write!(f, "{} {}", command, args.join(" "))
49             }
50         }
51     }
52 }
53
54 /// Flycheck wraps the shared state and communication machinery used for
55 /// running `cargo check` (or other compatible command) and providing
56 /// diagnostics based on the output.
57 /// The spawned thread is shut down when this struct is dropped.
58 #[derive(Debug)]
59 pub struct FlycheckHandle {
60     // XXX: drop order is significant
61     sender: Sender<Restart>,
62     _thread: jod_thread::JoinHandle,
63     id: usize,
64 }
65
66 impl FlycheckHandle {
67     pub fn spawn(
68         id: usize,
69         sender: Box<dyn Fn(Message) + Send>,
70         config: FlycheckConfig,
71         workspace_root: AbsPathBuf,
72     ) -> FlycheckHandle {
73         let actor = FlycheckActor::new(id, sender, config, workspace_root);
74         let (sender, receiver) = unbounded::<Restart>();
75         let thread = jod_thread::Builder::new()
76             .name("Flycheck".to_owned())
77             .spawn(move || actor.run(receiver))
78             .expect("failed to spawn thread");
79         FlycheckHandle { id, sender, _thread: thread }
80     }
81
82     /// Schedule a re-start of the cargo check worker.
83     pub fn restart(&self) {
84         self.sender.send(Restart::Yes).unwrap();
85     }
86
87     /// Stop this cargo check worker.
88     pub fn cancel(&self) {
89         self.sender.send(Restart::No).unwrap();
90     }
91
92     pub fn id(&self) -> usize {
93         self.id
94     }
95 }
96
97 pub enum Message {
98     /// Request adding a diagnostic with fixes included to a file
99     AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
100
101     /// Request check progress notification to client
102     Progress {
103         /// Flycheck instance ID
104         id: usize,
105         progress: Progress,
106     },
107 }
108
109 impl fmt::Debug for Message {
110     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111         match self {
112             Message::AddDiagnostic { id, workspace_root, diagnostic } => f
113                 .debug_struct("AddDiagnostic")
114                 .field("id", id)
115                 .field("workspace_root", workspace_root)
116                 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
117                 .finish(),
118             Message::Progress { id, progress } => {
119                 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
120             }
121         }
122     }
123 }
124
125 #[derive(Debug)]
126 pub enum Progress {
127     DidStart,
128     DidCheckCrate(String),
129     DidFinish(io::Result<()>),
130     DidCancel,
131     DidFailToRestart(String),
132 }
133
134 enum Restart {
135     Yes,
136     No,
137 }
138
139 struct FlycheckActor {
140     id: usize,
141     sender: Box<dyn Fn(Message) + Send>,
142     config: FlycheckConfig,
143     workspace_root: AbsPathBuf,
144     /// CargoHandle exists to wrap around the communication needed to be able to
145     /// run `cargo check` without blocking. Currently the Rust standard library
146     /// doesn't provide a way to read sub-process output without blocking, so we
147     /// have to wrap sub-processes output handling in a thread and pass messages
148     /// back over a channel.
149     cargo_handle: Option<CargoHandle>,
150 }
151
152 enum Event {
153     Restart(Restart),
154     CheckEvent(Option<CargoMessage>),
155 }
156
157 impl FlycheckActor {
158     fn new(
159         id: usize,
160         sender: Box<dyn Fn(Message) + Send>,
161         config: FlycheckConfig,
162         workspace_root: AbsPathBuf,
163     ) -> FlycheckActor {
164         tracing::info!(%id, ?workspace_root, "Spawning flycheck");
165         FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
166     }
167     fn progress(&self, progress: Progress) {
168         self.send(Message::Progress { id: self.id, progress });
169     }
170     fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
171         let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
172         if let Ok(msg) = inbox.try_recv() {
173             // give restarts a preference so check outputs don't block a restart or stop
174             return Some(Event::Restart(msg));
175         }
176         select! {
177             recv(inbox) -> msg => msg.ok().map(Event::Restart),
178             recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
179         }
180     }
181     fn run(mut self, inbox: Receiver<Restart>) {
182         'event: while let Some(event) = self.next_event(&inbox) {
183             match event {
184                 Event::Restart(Restart::No) => {
185                     self.cancel_check_process();
186                 }
187                 Event::Restart(Restart::Yes) => {
188                     // Cancel the previously spawned process
189                     self.cancel_check_process();
190                     while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
191                         // restart chained with a stop, so just cancel
192                         if let Restart::No = restart {
193                             continue 'event;
194                         }
195                     }
196
197                     let command = self.check_command();
198                     tracing::debug!(?command, "will restart flycheck");
199                     match CargoHandle::spawn(command) {
200                         Ok(cargo_handle) => {
201                             tracing::debug!(
202                                 command = ?self.check_command(),
203                                 "did  restart flycheck"
204                             );
205                             self.cargo_handle = Some(cargo_handle);
206                             self.progress(Progress::DidStart);
207                         }
208                         Err(error) => {
209                             self.progress(Progress::DidFailToRestart(format!(
210                                 "Failed to run the following command: {:?} error={}",
211                                 self.check_command(),
212                                 error
213                             )));
214                         }
215                     }
216                 }
217                 Event::CheckEvent(None) => {
218                     tracing::debug!(flycheck_id = self.id, "flycheck finished");
219
220                     // Watcher finished
221                     let cargo_handle = self.cargo_handle.take().unwrap();
222                     let res = cargo_handle.join();
223                     if res.is_err() {
224                         tracing::error!(
225                             "Flycheck failed to run the following command: {:?}",
226                             self.check_command()
227                         );
228                     }
229                     self.progress(Progress::DidFinish(res));
230                 }
231                 Event::CheckEvent(Some(message)) => match message {
232                     CargoMessage::CompilerArtifact(msg) => {
233                         self.progress(Progress::DidCheckCrate(msg.target.name));
234                     }
235
236                     CargoMessage::Diagnostic(msg) => {
237                         self.send(Message::AddDiagnostic {
238                             id: self.id,
239                             workspace_root: self.workspace_root.clone(),
240                             diagnostic: msg,
241                         });
242                     }
243                 },
244             }
245         }
246         // If we rerun the thread, we need to discard the previous check results first
247         self.cancel_check_process();
248     }
249
250     fn cancel_check_process(&mut self) {
251         if let Some(cargo_handle) = self.cargo_handle.take() {
252             tracing::debug!(
253                 command = ?self.check_command(),
254                 "did  cancel flycheck"
255             );
256             cargo_handle.cancel();
257             self.progress(Progress::DidCancel);
258         }
259     }
260
261     fn check_command(&self) -> Command {
262         let mut cmd = match &self.config {
263             FlycheckConfig::CargoCommand {
264                 command,
265                 target_triple,
266                 no_default_features,
267                 all_targets,
268                 all_features,
269                 extra_args,
270                 features,
271                 extra_env,
272             } => {
273                 let mut cmd = Command::new(toolchain::cargo());
274                 cmd.arg(command);
275                 cmd.current_dir(&self.workspace_root);
276                 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
277                     .arg(self.workspace_root.join("Cargo.toml").as_os_str());
278
279                 if let Some(target) = target_triple {
280                     cmd.args(&["--target", target.as_str()]);
281                 }
282                 if *all_targets {
283                     cmd.arg("--all-targets");
284                 }
285                 if *all_features {
286                     cmd.arg("--all-features");
287                 } else {
288                     if *no_default_features {
289                         cmd.arg("--no-default-features");
290                     }
291                     if !features.is_empty() {
292                         cmd.arg("--features");
293                         cmd.arg(features.join(" "));
294                     }
295                 }
296                 cmd.args(extra_args);
297                 cmd.envs(extra_env);
298                 cmd
299             }
300             FlycheckConfig::CustomCommand { command, args, extra_env } => {
301                 let mut cmd = Command::new(command);
302                 cmd.args(args);
303                 cmd.envs(extra_env);
304                 cmd
305             }
306         };
307         cmd.current_dir(&self.workspace_root);
308         cmd
309     }
310
311     fn send(&self, check_task: Message) {
312         (self.sender)(check_task);
313     }
314 }
315
316 /// A handle to a cargo process used for fly-checking.
317 struct CargoHandle {
318     /// The handle to the actual cargo process. As we cannot cancel directly from with
319     /// a read syscall dropping and therefor terminating the process is our best option.
320     child: JodChild,
321     thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
322     receiver: Receiver<CargoMessage>,
323 }
324
325 impl CargoHandle {
326     fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
327         command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
328         let mut child = JodChild::spawn(command)?;
329
330         let stdout = child.stdout.take().unwrap();
331         let stderr = child.stderr.take().unwrap();
332
333         let (sender, receiver) = unbounded();
334         let actor = CargoActor::new(sender, stdout, stderr);
335         let thread = jod_thread::Builder::new()
336             .name("CargoHandle".to_owned())
337             .spawn(move || actor.run())
338             .expect("failed to spawn thread");
339         Ok(CargoHandle { child, thread, receiver })
340     }
341
342     fn cancel(mut self) {
343         let _ = self.child.kill();
344         let _ = self.child.wait();
345     }
346
347     fn join(mut self) -> io::Result<()> {
348         let _ = self.child.kill();
349         let exit_status = self.child.wait()?;
350         let (read_at_least_one_message, error) = self.thread.join()?;
351         if read_at_least_one_message || exit_status.success() {
352             Ok(())
353         } else {
354             Err(io::Error::new(io::ErrorKind::Other, format!(
355                 "Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
356                 exit_status, error
357             )))
358         }
359     }
360 }
361
362 struct CargoActor {
363     sender: Sender<CargoMessage>,
364     stdout: ChildStdout,
365     stderr: ChildStderr,
366 }
367
368 impl CargoActor {
369     fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
370         CargoActor { sender, stdout, stderr }
371     }
372
373     fn run(self) -> io::Result<(bool, String)> {
374         // We manually read a line at a time, instead of using serde's
375         // stream deserializers, because the deserializer cannot recover
376         // from an error, resulting in it getting stuck, because we try to
377         // be resilient against failures.
378         //
379         // Because cargo only outputs one JSON object per line, we can
380         // simply skip a line if it doesn't parse, which just ignores any
381         // erroneous output.
382
383         let mut error = String::new();
384         let mut read_at_least_one_message = false;
385         let output = streaming_output(
386             self.stdout,
387             self.stderr,
388             &mut |line| {
389                 read_at_least_one_message = true;
390
391                 // Try to deserialize a message from Cargo or Rustc.
392                 let mut deserializer = serde_json::Deserializer::from_str(line);
393                 deserializer.disable_recursion_limit();
394                 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
395                     match message {
396                         // Skip certain kinds of messages to only spend time on what's useful
397                         JsonMessage::Cargo(message) => match message {
398                             cargo_metadata::Message::CompilerArtifact(artifact)
399                                 if !artifact.fresh =>
400                             {
401                                 self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
402                             }
403                             cargo_metadata::Message::CompilerMessage(msg) => {
404                                 self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
405                             }
406                             _ => (),
407                         },
408                         JsonMessage::Rustc(message) => {
409                             self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
410                         }
411                     }
412                 }
413             },
414             &mut |line| {
415                 error.push_str(line);
416                 error.push('\n');
417             },
418         );
419         match output {
420             Ok(_) => Ok((read_at_least_one_message, error)),
421             Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
422         }
423     }
424 }
425
426 enum CargoMessage {
427     CompilerArtifact(cargo_metadata::Artifact),
428     Diagnostic(Diagnostic),
429 }
430
431 #[derive(Deserialize)]
432 #[serde(untagged)]
433 enum JsonMessage {
434     Cargo(cargo_metadata::Message),
435     Rustc(Diagnostic),
436 }