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