]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/flycheck/src/lib.rs
Rollup merge of #102085 - chenyukang:code-refactor, r=cjgillot
[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         select! {
173             recv(inbox) -> msg => msg.ok().map(Event::Restart),
174             recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
175         }
176     }
177     fn run(mut self, inbox: Receiver<Restart>) {
178         while let Some(event) = self.next_event(&inbox) {
179             match event {
180                 Event::Restart(Restart::No) => {
181                     self.cancel_check_process();
182                 }
183                 Event::Restart(Restart::Yes) => {
184                     // Cancel the previously spawned process
185                     self.cancel_check_process();
186                     while let Ok(_) = inbox.recv_timeout(Duration::from_millis(50)) {}
187
188                     let command = self.check_command();
189                     tracing::debug!(?command, "will restart flycheck");
190                     match CargoHandle::spawn(command) {
191                         Ok(cargo_handle) => {
192                             tracing::debug!(
193                                 command = ?self.check_command(),
194                                 "did  restart flycheck"
195                             );
196                             self.cargo_handle = Some(cargo_handle);
197                             self.progress(Progress::DidStart);
198                         }
199                         Err(error) => {
200                             self.progress(Progress::DidFailToRestart(format!(
201                                 "Failed to run the following command: {:?} error={}",
202                                 self.check_command(),
203                                 error
204                             )));
205                         }
206                     }
207                 }
208                 Event::CheckEvent(None) => {
209                     tracing::debug!(flycheck_id = self.id, "flycheck finished");
210
211                     // Watcher finished
212                     let cargo_handle = self.cargo_handle.take().unwrap();
213                     let res = cargo_handle.join();
214                     if res.is_err() {
215                         tracing::error!(
216                             "Flycheck failed to run the following command: {:?}",
217                             self.check_command()
218                         );
219                     }
220                     self.progress(Progress::DidFinish(res));
221                 }
222                 Event::CheckEvent(Some(message)) => match message {
223                     CargoMessage::CompilerArtifact(msg) => {
224                         self.progress(Progress::DidCheckCrate(msg.target.name));
225                     }
226
227                     CargoMessage::Diagnostic(msg) => {
228                         self.send(Message::AddDiagnostic {
229                             id: self.id,
230                             workspace_root: self.workspace_root.clone(),
231                             diagnostic: msg,
232                         });
233                     }
234                 },
235             }
236         }
237         // If we rerun the thread, we need to discard the previous check results first
238         self.cancel_check_process();
239     }
240
241     fn cancel_check_process(&mut self) {
242         if let Some(cargo_handle) = self.cargo_handle.take() {
243             tracing::debug!(
244                 command = ?self.check_command(),
245                 "did  cancel flycheck"
246             );
247             cargo_handle.cancel();
248             self.progress(Progress::DidCancel);
249         }
250     }
251
252     fn check_command(&self) -> Command {
253         let mut cmd = match &self.config {
254             FlycheckConfig::CargoCommand {
255                 command,
256                 target_triple,
257                 no_default_features,
258                 all_targets,
259                 all_features,
260                 extra_args,
261                 features,
262                 extra_env,
263             } => {
264                 let mut cmd = Command::new(toolchain::cargo());
265                 cmd.arg(command);
266                 cmd.current_dir(&self.workspace_root);
267                 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
268                     .arg(self.workspace_root.join("Cargo.toml").as_os_str());
269
270                 if let Some(target) = target_triple {
271                     cmd.args(&["--target", target.as_str()]);
272                 }
273                 if *all_targets {
274                     cmd.arg("--all-targets");
275                 }
276                 if *all_features {
277                     cmd.arg("--all-features");
278                 } else {
279                     if *no_default_features {
280                         cmd.arg("--no-default-features");
281                     }
282                     if !features.is_empty() {
283                         cmd.arg("--features");
284                         cmd.arg(features.join(" "));
285                     }
286                 }
287                 cmd.args(extra_args);
288                 cmd.envs(extra_env);
289                 cmd
290             }
291             FlycheckConfig::CustomCommand { command, args, extra_env } => {
292                 let mut cmd = Command::new(command);
293                 cmd.args(args);
294                 cmd.envs(extra_env);
295                 cmd
296             }
297         };
298         cmd.current_dir(&self.workspace_root);
299         cmd
300     }
301
302     fn send(&self, check_task: Message) {
303         (self.sender)(check_task);
304     }
305 }
306
307 /// A handle to a cargo process used for fly-checking.
308 struct CargoHandle {
309     /// The handle to the actual cargo process. As we cannot cancel directly from with
310     /// a read syscall dropping and therefor terminating the process is our best option.
311     child: JodChild,
312     thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
313     receiver: Receiver<CargoMessage>,
314 }
315
316 impl CargoHandle {
317     fn spawn(mut command: Command) -> std::io::Result<CargoHandle> {
318         command.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::null());
319         let mut child = JodChild::spawn(command)?;
320
321         let stdout = child.stdout.take().unwrap();
322         let stderr = child.stderr.take().unwrap();
323
324         let (sender, receiver) = unbounded();
325         let actor = CargoActor::new(sender, stdout, stderr);
326         let thread = jod_thread::Builder::new()
327             .name("CargoHandle".to_owned())
328             .spawn(move || actor.run())
329             .expect("failed to spawn thread");
330         Ok(CargoHandle { child, thread, receiver })
331     }
332
333     fn cancel(mut self) {
334         let _ = self.child.kill();
335         let _ = self.child.wait();
336     }
337
338     fn join(mut self) -> io::Result<()> {
339         let _ = self.child.kill();
340         let exit_status = self.child.wait()?;
341         let (read_at_least_one_message, error) = self.thread.join()?;
342         if read_at_least_one_message || exit_status.success() {
343             Ok(())
344         } else {
345             Err(io::Error::new(io::ErrorKind::Other, format!(
346                 "Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
347                 exit_status, error
348             )))
349         }
350     }
351 }
352
353 struct CargoActor {
354     sender: Sender<CargoMessage>,
355     stdout: ChildStdout,
356     stderr: ChildStderr,
357 }
358
359 impl CargoActor {
360     fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
361         CargoActor { sender, stdout, stderr }
362     }
363
364     fn run(self) -> io::Result<(bool, String)> {
365         // We manually read a line at a time, instead of using serde's
366         // stream deserializers, because the deserializer cannot recover
367         // from an error, resulting in it getting stuck, because we try to
368         // be resilient against failures.
369         //
370         // Because cargo only outputs one JSON object per line, we can
371         // simply skip a line if it doesn't parse, which just ignores any
372         // erroneous output.
373
374         let mut error = String::new();
375         let mut read_at_least_one_message = false;
376         let output = streaming_output(
377             self.stdout,
378             self.stderr,
379             &mut |line| {
380                 read_at_least_one_message = true;
381
382                 // Try to deserialize a message from Cargo or Rustc.
383                 let mut deserializer = serde_json::Deserializer::from_str(line);
384                 deserializer.disable_recursion_limit();
385                 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
386                     match message {
387                         // Skip certain kinds of messages to only spend time on what's useful
388                         JsonMessage::Cargo(message) => match message {
389                             cargo_metadata::Message::CompilerArtifact(artifact)
390                                 if !artifact.fresh =>
391                             {
392                                 self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
393                             }
394                             cargo_metadata::Message::CompilerMessage(msg) => {
395                                 self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
396                             }
397                             _ => (),
398                         },
399                         JsonMessage::Rustc(message) => {
400                             self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
401                         }
402                     }
403                 }
404             },
405             &mut |line| {
406                 error.push_str(line);
407                 error.push('\n');
408             },
409         );
410         match output {
411             Ok(_) => Ok((read_at_least_one_message, error)),
412             Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
413         }
414     }
415 }
416
417 enum CargoMessage {
418     CompilerArtifact(cargo_metadata::Artifact),
419     Diagnostic(Diagnostic),
420 }
421
422 #[derive(Deserialize)]
423 #[serde(untagged)]
424 enum JsonMessage {
425     Cargo(cargo_metadata::Message),
426     Rustc(Diagnostic),
427 }