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.
5 #![warn(rust_2018_idioms, unused_lifetimes, semicolon_in_expressions_from_macros)]
9 process::{ChildStderr, ChildStdout, Command, Stdio},
13 use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
14 use paths::AbsPathBuf;
15 use serde::Deserialize;
16 use stdx::{process::streaming_output, JodChild};
18 pub use cargo_metadata::diagnostic::{
19 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
20 DiagnosticSpanMacroExpansion,
23 #[derive(Clone, Debug, PartialEq, Eq)]
24 pub enum FlycheckConfig {
27 target_triple: Option<String>,
29 no_default_features: bool,
31 features: Vec<String>,
32 extra_args: Vec<String>,
40 impl fmt::Display for FlycheckConfig {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {}", command),
44 FlycheckConfig::CustomCommand { command, args } => {
45 write!(f, "{} {}", command, args.join(" "))
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.
56 pub struct FlycheckHandle {
57 // XXX: drop order is significant
58 sender: Sender<Restart>,
59 _thread: jod_thread::JoinHandle,
66 sender: Box<dyn Fn(Message) + Send>,
67 config: FlycheckConfig,
68 workspace_root: AbsPathBuf,
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 }
79 /// Schedule a re-start of the cargo check worker.
80 pub fn update(&self) {
81 self.sender.send(Restart).unwrap();
84 pub fn id(&self) -> usize {
90 /// Request adding a diagnostic with fixes included to a file
91 AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
93 /// Request check progress notification to client
95 /// Flycheck instance ID
101 impl fmt::Debug for Message {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 Message::AddDiagnostic { id, workspace_root, diagnostic } => f
105 .debug_struct("AddDiagnostic")
107 .field("workspace_root", workspace_root)
108 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
110 Message::Progress { id, progress } => {
111 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
120 DidCheckCrate(String),
121 DidFinish(io::Result<()>),
127 struct FlycheckActor {
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>,
142 CheckEvent(Option<CargoMessage>),
148 sender: Box<dyn Fn(Message) + Send>,
149 config: FlycheckConfig,
150 workspace_root: AbsPathBuf,
152 FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
154 fn progress(&self, progress: Progress) {
155 self.send(Message::Progress { id: self.id, progress });
157 fn next_event(&self, inbox: &Receiver<Restart>) -> Option<Event> {
158 let check_chan = self.cargo_handle.as_ref().map(|cargo| &cargo.receiver);
160 recv(inbox) -> msg => msg.ok().map(Event::Restart),
161 recv(check_chan.unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
164 fn run(mut self, inbox: Receiver<Restart>) {
165 while let Some(event) = self.next_event(&inbox) {
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)) {}
172 let command = self.check_command();
173 tracing::debug!(?command, "will restart flycheck");
174 match CargoHandle::spawn(command) {
175 Ok(cargo_handle) => {
177 command = ?self.check_command(),
178 "did restart flycheck"
180 self.cargo_handle = Some(cargo_handle);
181 self.progress(Progress::DidStart);
185 command = ?self.check_command(),
186 %error, "failed to restart flycheck"
191 Event::CheckEvent(None) => {
192 tracing::debug!(flycheck_id = self.id, "flycheck finished");
195 let cargo_handle = self.cargo_handle.take().unwrap();
196 let res = cargo_handle.join();
199 "Flycheck failed to run the following command: {:?}",
203 self.progress(Progress::DidFinish(res));
205 Event::CheckEvent(Some(message)) => match message {
206 CargoMessage::CompilerArtifact(msg) => {
207 self.progress(Progress::DidCheckCrate(msg.target.name));
210 CargoMessage::Diagnostic(msg) => {
211 self.send(Message::AddDiagnostic {
213 workspace_root: self.workspace_root.clone(),
220 // If we rerun the thread, we need to discard the previous check results first
221 self.cancel_check_process();
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);
231 fn check_command(&self) -> Command {
232 let mut cmd = match &self.config {
233 FlycheckConfig::CargoCommand {
242 let mut cmd = Command::new(toolchain::cargo());
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());
248 if let Some(target) = target_triple {
249 cmd.args(&["--target", target.as_str()]);
252 cmd.arg("--all-targets");
255 cmd.arg("--all-features");
257 if *no_default_features {
258 cmd.arg("--no-default-features");
260 if !features.is_empty() {
261 cmd.arg("--features");
262 cmd.arg(features.join(" "));
265 cmd.args(extra_args);
268 FlycheckConfig::CustomCommand { command, args } => {
269 let mut cmd = Command::new(command);
274 cmd.current_dir(&self.workspace_root);
278 fn send(&self, check_task: Message) {
279 (self.sender)(check_task);
283 /// A handle to a cargo process used for fly-checking.
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.
288 thread: jod_thread::JoinHandle<io::Result<(bool, String)>>,
289 receiver: Receiver<CargoMessage>,
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)?;
297 let stdout = child.stdout.take().unwrap();
298 let stderr = child.stderr.take().unwrap();
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 })
309 fn cancel(mut self) {
310 let _ = self.child.kill();
311 let _ = self.child.wait();
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() {
321 Err(io::Error::new(io::ErrorKind::Other, format!(
322 "Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
330 sender: Sender<CargoMessage>,
336 fn new(sender: Sender<CargoMessage>, stdout: ChildStdout, stderr: ChildStderr) -> CargoActor {
337 CargoActor { sender, stdout, stderr }
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.
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
350 let mut error = String::new();
351 let mut read_at_least_one_message = false;
352 let output = streaming_output(
356 read_at_least_one_message = true;
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) {
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 =>
368 self.sender.send(CargoMessage::CompilerArtifact(artifact)).unwrap();
370 cargo_metadata::Message::CompilerMessage(msg) => {
371 self.sender.send(CargoMessage::Diagnostic(msg.message)).unwrap();
375 JsonMessage::Rustc(message) => {
376 self.sender.send(CargoMessage::Diagnostic(message)).unwrap();
382 error.push_str(line);
387 Ok(_) => Ok((read_at_least_one_message, error)),
388 Err(e) => Err(io::Error::new(e.kind(), format!("{:?}: {}", e, error))),
394 CompilerArtifact(cargo_metadata::Artifact),
395 Diagnostic(Diagnostic),
398 #[derive(Deserialize)]
401 Cargo(cargo_metadata::Message),