]> git.lizzy.rs Git - rust.git/blob - crates/flycheck/src/lib.rs
Use package root as `cargo check` working directory
[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.current_dir(&self.workspace_root);
219                 cmd.args(&["--workspace", "--message-format=json", "--manifest-path"])
220                     .arg(self.workspace_root.join("Cargo.toml"));
221
222                 if let Some(target) = target_triple {
223                     cmd.args(&["--target", target.as_str()]);
224                 }
225                 if *all_targets {
226                     cmd.arg("--all-targets");
227                 }
228                 if *all_features {
229                     cmd.arg("--all-features");
230                 } else {
231                     if *no_default_features {
232                         cmd.arg("--no-default-features");
233                     }
234                     if !features.is_empty() {
235                         cmd.arg("--features");
236                         cmd.arg(features.join(" "));
237                     }
238                 }
239                 cmd.args(extra_args);
240                 cmd
241             }
242             FlycheckConfig::CustomCommand { command, args } => {
243                 let mut cmd = Command::new(command);
244                 cmd.args(args);
245                 cmd
246             }
247         };
248         cmd.current_dir(&self.workspace_root);
249         cmd
250     }
251
252     fn send(&self, check_task: Message) {
253         (self.sender)(check_task)
254     }
255 }
256
257 struct CargoHandle {
258     child: JodChild,
259     #[allow(unused)]
260     thread: jod_thread::JoinHandle<io::Result<bool>>,
261     receiver: Receiver<CargoMessage>,
262 }
263
264 impl CargoHandle {
265     fn spawn(mut child: JodChild) -> CargoHandle {
266         let child_stdout = child.stdout.take().unwrap();
267         let (sender, receiver) = unbounded();
268         let actor = CargoActor::new(child_stdout, sender);
269         let thread = jod_thread::spawn(move || actor.run());
270         CargoHandle { child, thread, receiver }
271     }
272     fn join(mut self) -> io::Result<()> {
273         // It is okay to ignore the result, as it only errors if the process is already dead
274         let _ = self.child.kill();
275         let exit_status = self.child.wait()?;
276         let read_at_least_one_message = self.thread.join()?;
277         if !exit_status.success() && !read_at_least_one_message {
278             // FIXME: Read the stderr to display the reason, see `read2()` reference in PR comment:
279             // https://github.com/rust-analyzer/rust-analyzer/pull/3632#discussion_r395605298
280             return Err(io::Error::new(
281                 io::ErrorKind::Other,
282                 format!(
283                     "Cargo watcher failed, the command produced no valid metadata (exit code: {:?})",
284                     exit_status
285                 ),
286             ));
287         }
288         Ok(())
289     }
290 }
291
292 struct CargoActor {
293     child_stdout: process::ChildStdout,
294     sender: Sender<CargoMessage>,
295 }
296
297 impl CargoActor {
298     fn new(child_stdout: process::ChildStdout, sender: Sender<CargoMessage>) -> CargoActor {
299         CargoActor { child_stdout, sender }
300     }
301     fn run(self) -> io::Result<bool> {
302         // We manually read a line at a time, instead of using serde's
303         // stream deserializers, because the deserializer cannot recover
304         // from an error, resulting in it getting stuck, because we try to
305         // be resilient against failures.
306         //
307         // Because cargo only outputs one JSON object per line, we can
308         // simply skip a line if it doesn't parse, which just ignores any
309         // erroneus output.
310         let stdout = BufReader::new(self.child_stdout);
311         let mut read_at_least_one_message = false;
312         for message in stdout.lines() {
313             let message = match message {
314                 Ok(message) => message,
315                 Err(err) => {
316                     log::error!("Invalid json from cargo check, ignoring ({})", err);
317                     continue;
318                 }
319             };
320
321             read_at_least_one_message = true;
322
323             // Try to deserialize a message from Cargo or Rustc.
324             let mut deserializer = serde_json::Deserializer::from_str(&message);
325             deserializer.disable_recursion_limit();
326             if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
327                 match message {
328                     // Skip certain kinds of messages to only spend time on what's useful
329                     JsonMessage::Cargo(message) => match message {
330                         cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
331                             self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap()
332                         }
333                         cargo_metadata::Message::CompilerMessage(msg) => {
334                             self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap()
335                         }
336
337                         cargo_metadata::Message::CompilerArtifact(_)
338                         | cargo_metadata::Message::BuildScriptExecuted(_)
339                         | cargo_metadata::Message::BuildFinished(_)
340                         | cargo_metadata::Message::TextLine(_)
341                         | _ => (),
342                     },
343                     JsonMessage::Rustc(message) => {
344                         self.sender.send(CargoMessage::Diagnostic(message)).unwrap()
345                     }
346                 }
347             }
348         }
349         Ok(read_at_least_one_message)
350     }
351 }
352
353 enum CargoMessage {
354     CompilerArtifact(cargo_metadata::Artifact),
355     Diagnostic(Diagnostic),
356 }
357
358 #[derive(Deserialize)]
359 #[serde(untagged)]
360 enum JsonMessage {
361     Cargo(cargo_metadata::Message),
362     Rustc(Diagnostic),
363 }