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