]> git.lizzy.rs Git - rust.git/blob - crates/flycheck/src/lib.rs
1682d8bde23548f38073682be12b6cd00dfdea5b
[rust.git] / 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 use std::{
6     fmt,
7     io::{self, BufRead, BufReader},
8     path::PathBuf,
9     process::{self, Command, Stdio},
10     time::Duration,
11 };
12
13 use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
14 use serde::Deserialize;
15 use stdx::JodChild;
16
17 pub use cargo_metadata::diagnostic::{
18     Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
19     DiagnosticSpanMacroExpansion,
20 };
21
22 #[derive(Clone, Debug, PartialEq, Eq)]
23 pub enum FlycheckConfig {
24     CargoCommand {
25         command: String,
26         target_triple: Option<String>,
27         all_targets: bool,
28         no_default_features: bool,
29         all_features: bool,
30         features: Vec<String>,
31         extra_args: Vec<String>,
32     },
33     CustomCommand {
34         command: String,
35         args: Vec<String>,
36     },
37 }
38
39 impl fmt::Display for FlycheckConfig {
40     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41         match self {
42             FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
43             FlycheckConfig::CustomCommand { command, args } => {
44                 write!(f, "{} {}", command, args.join(" "))
45             }
46         }
47     }
48 }
49
50 /// Flycheck wraps the shared state and communication machinery used for
51 /// running `cargo check` (or other compatible command) and providing
52 /// diagnostics based on the output.
53 /// The spawned thread is shut down when this struct is dropped.
54 #[derive(Debug)]
55 pub struct FlycheckHandle {
56     // XXX: drop order is significant
57     sender: Sender<Restart>,
58     thread: jod_thread::JoinHandle,
59 }
60
61 impl FlycheckHandle {
62     pub fn spawn(
63         id: usize,
64         sender: Box<dyn Fn(Message) + Send>,
65         config: FlycheckConfig,
66         workspace_root: PathBuf,
67     ) -> FlycheckHandle {
68         let actor = FlycheckActor::new(id, sender, config, workspace_root);
69         let (sender, receiver) = unbounded::<Restart>();
70         let thread = jod_thread::spawn(move || actor.run(receiver));
71         FlycheckHandle { sender, thread }
72     }
73
74     /// Schedule a re-start of the cargo check worker.
75     pub fn update(&self) {
76         self.sender.send(Restart).unwrap();
77     }
78 }
79
80 pub enum Message {
81     /// Request adding a diagnostic with fixes included to a file
82     AddDiagnostic { workspace_root: PathBuf, diagnostic: Diagnostic },
83
84     /// Request check progress notification to client
85     Progress {
86         /// Flycheck instance ID
87         id: usize,
88         progress: Progress,
89     },
90 }
91
92 impl fmt::Debug for Message {
93     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94         match self {
95             Message::AddDiagnostic { workspace_root, diagnostic } => f
96                 .debug_struct("AddDiagnostic")
97                 .field("workspace_root", workspace_root)
98                 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
99                 .finish(),
100             Message::Progress { id, progress } => {
101                 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
102             }
103         }
104     }
105 }
106
107 #[derive(Debug)]
108 pub enum Progress {
109     DidStart,
110     DidCheckCrate(String),
111     DidFinish(io::Result<()>),
112     DidCancel,
113 }
114
115 struct Restart;
116
117 struct FlycheckActor {
118     id: usize,
119     sender: Box<dyn Fn(Message) + Send>,
120     config: FlycheckConfig,
121     workspace_root: PathBuf,
122     /// WatchThread exists to wrap around the communication needed to be able to
123     /// run `cargo check` without blocking. Currently the Rust standard library
124     /// doesn't provide a way to read sub-process output without blocking, so we
125     /// have to wrap sub-processes output handling in a thread and pass messages
126     /// back over a channel.
127     cargo_handle: Option<CargoHandle>,
128 }
129
130 enum Event {
131     Restart(Restart),
132     CheckEvent(Option<CargoMessage>),
133 }
134
135 impl FlycheckActor {
136     fn new(
137         id: usize,
138         sender: Box<dyn Fn(Message) + Send>,
139         config: FlycheckConfig,
140         workspace_root: PathBuf,
141     ) -> FlycheckActor {
142         FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
143     }
144     fn progress(&self, progress: Progress) {
145         self.send(Message::Progress { id: self.id, progress });
146     }
147     fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
148         let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
149         select! {
150             recv(inbox) -> msg => msg.ok().map(Event::Restart),
151             recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
152         }
153     }
154     fn run(mut self, inbox: Receiver<Restart>) {
155         while let Some(event) = self.next_event(&inbox) {
156             match event {
157                 Event::Restart(Restart) => {
158                     while let Ok(Restart) = inbox.recv_timeout(Duration::from_millis(50)) {}
159
160                     self.cancel_check_process();
161
162                     let mut command = self.check_command();
163                     log::info!("restart flycheck {:?}", command);
164                     command.stdout(Stdio::piped()).stderr(Stdio::null()).stdin(Stdio::null());
165                     if let Ok(child) = command.spawn().map(JodChild) {
166                         self.cargo_handle = Some(CargoHandle::spawn(child));
167                         self.progress(Progress::DidStart);
168                     }
169                 }
170                 Event::CheckEvent(None) => {
171                     // Watcher finished, replace it with a never channel to
172                     // avoid busy-waiting.
173                     let cargo_handle = self.cargo_handle.take().unwrap();
174                     let res = cargo_handle.join();
175                     if res.is_err() {
176                         log::error!(
177                             "Flycheck failed to run the following command: {:?}",
178                             self.check_command()
179                         )
180                     }
181                     self.progress(Progress::DidFinish(res));
182                 }
183                 Event::CheckEvent(Some(message)) => match message {
184                     CargoMessage::CompilerArtifact(msg) => {
185                         self.progress(Progress::DidCheckCrate(msg.target.name));
186                     }
187
188                     CargoMessage::Diagnostic(msg) => {
189                         self.send(Message::AddDiagnostic {
190                             workspace_root: self.workspace_root.clone(),
191                             diagnostic: msg,
192                         });
193                     }
194                 },
195             }
196         }
197         // If we rerun the thread, we need to discard the previous check results first
198         self.cancel_check_process();
199     }
200     fn cancel_check_process(&mut self) {
201         if self.cargo_handle.take().is_some() {
202             self.progress(Progress::DidCancel);
203         }
204     }
205     fn check_command(&self) -> Command {
206         let mut cmd = match &self.config {
207             FlycheckConfig::CargoCommand {
208                 command,
209                 target_triple,
210                 no_default_features,
211                 all_targets,
212                 all_features,
213                 extra_args,
214                 features,
215             } => {
216                 let mut cmd = Command::new(toolchain::cargo());
217                 cmd.arg(command);
218                 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
219                     .arg(self.workspace_root.join("Cargo.toml"));
220
221                 if let Some(target) = target_triple {
222                     cmd.args(&["--target", target.as_str()]);
223                 }
224                 if *all_targets {
225                     cmd.arg("--all-targets");
226                 }
227                 if *all_features {
228                     cmd.arg("--all-features");
229                 } else {
230                     if *no_default_features {
231                         cmd.arg("--no-default-features");
232                     }
233                     if !features.is_empty() {
234                         cmd.arg("--features");
235                         cmd.arg(features.join(" "));
236                     }
237                 }
238                 cmd.args(extra_args);
239                 cmd
240             }
241             FlycheckConfig::CustomCommand { command, args } => {
242                 let mut cmd = Command::new(command);
243                 cmd.args(args);
244                 cmd
245             }
246         };
247         cmd.current_dir(&self.workspace_root);
248         cmd
249     }
250
251     fn send(&self, check_task: Message) {
252         (self.sender)(check_task)
253     }
254 }
255
256 struct CargoHandle {
257     child: JodChild,
258     #[allow(unused)]
259     thread: jod_thread::JoinHandle<io::Result<bool>>,
260     receiver: Receiver<CargoMessage>,
261 }
262
263 impl CargoHandle {
264     fn spawn(mut child: JodChild) -> CargoHandle {
265         let child_stdout = child.stdout.take().unwrap();
266         let (sender, receiver) = unbounded();
267         let actor = CargoActor::new(child_stdout, sender);
268         let thread = jod_thread::spawn(move || actor.run());
269         CargoHandle { child, thread, receiver }
270     }
271     fn join(mut self) -> io::Result<()> {
272         // It is okay to ignore the result, as it only errors if the process is already dead
273         let _ = self.child.kill();
274         let exit_status = self.child.wait()?;
275         let read_at_least_one_message = self.thread.join()?;
276         if !exit_status.success() && !read_at_least_one_message {
277             // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
278             // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
279             return Err(io::Error::new(
280                 io::ErrorKind::Other,
281                 format!(
282                     "Cargo watcher failed, the command produced no valid metadata (exit code: {:?})",
283                     exit_status
284                 ),
285             ));
286         }
287         Ok(())
288     }
289 }
290
291 struct CargoActor {
292     child_stdout: process::ChildStdout,
293     sender: Sender<CargoMessage>,
294 }
295
296 impl CargoActor {
297     fn new(child_stdout: process::ChildStdout, sender: Sender<CargoMessage>) -> CargoActor {
298         CargoActor { child_stdout, sender }
299     }
300     fn run(self) -> io::Result<bool> {
301         // We manually read a line at a time, instead of using serde's
302         // stream deserializers, because the deserializer cannot recover
303         // from an error, resulting in it getting stuck, because we try to
304         // be resilient against failures.
305         //
306         // Because cargo only outputs one JSON object per line, we can
307         // simply skip a line if it doesn't parse, which just ignores any
308         // erroneus output.
309         let stdout = BufReader::new(self.child_stdout);
310         let mut read_at_least_one_message = false;
311         for message in stdout.lines() {
312             let message = match message {
313                 Ok(message) => message,
314                 Err(err) => {
315                     log::error!("Invalid json from cargo check, ignoring ({})", err);
316                     continue;
317                 }
318             };
319
320             read_at_least_one_message = true;
321
322             // Try to deserialize a message from Cargo or Rustc.
323             let mut deserializer = serde_json::Deserializer::from_str(&message);
324             deserializer.disable_recursion_limit();
325             if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
326                 match message {
327                     // Skip certain kinds of messages to only spend time on what's useful
328                     JsonMessage::Cargo(message) => match message {
329                         cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
330                             self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap()
331                         }
332                         cargo_metadata::Message::CompilerMessage(msg) => {
333                             self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap()
334                         }
335
336                         cargo_metadata::Message::CompilerArtifact(_)
337                         | cargo_metadata::Message::BuildScriptExecuted(_)
338                         | cargo_metadata::Message::BuildFinished(_)
339                         | cargo_metadata::Message::TextLine(_)
340                         | _ => (),
341                     },
342                     JsonMessage::Rustc(message) => {
343                         self.sender.send(CargoMessage::Diagnostic(message)).unwrap()
344                     }
345                 }
346             }
347         }
348         Ok(read_at_least_one_message)
349     }
350 }
351
352 enum CargoMessage {
353     CompilerArtifact(cargo_metadata::Artifact),
354     Diagnostic(Diagnostic),
355 }
356
357 #[derive(Deserialize)]
358 #[serde(untagged)]
359 enum JsonMessage {
360     Cargo(cargo_metadata::Message),
361     Rustc(Diagnostic),
362 }