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
,
65 sender
: Box
<dyn Fn(Message
) + Send
>,
66 config
: FlycheckConfig
,
67 workspace_root
: AbsPathBuf
,
69 let actor
= FlycheckActor
::new(id
, sender
, config
, workspace_root
);
70 let (sender
, receiver
) = unbounded
::<Restart
>();
71 let thread
= jod_thread
::Builder
::new()
72 .name("Flycheck".to_owned())
73 .spawn(move || actor
.run(receiver
))
74 .expect("failed to spawn thread");
75 FlycheckHandle { sender, _thread: thread }
78 /// Schedule a re-start of the cargo check worker.
79 pub fn update(&self) {
80 self.sender
.send(Restart
).unwrap();
85 /// Request adding a diagnostic with fixes included to a file
86 AddDiagnostic { workspace_root: AbsPathBuf, diagnostic: Diagnostic }
,
88 /// Request check progress notification to client
90 /// Flycheck instance ID
96 impl fmt
::Debug
for Message
{
97 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
99 Message
::AddDiagnostic { workspace_root, diagnostic }
=> f
100 .debug_struct("AddDiagnostic")
101 .field("workspace_root", workspace_root
)
102 .field("diagnostic_code", &diagnostic
.code
.as_ref().map(|it
| &it
.code
))
104 Message
::Progress { id, progress }
=> {
105 f
.debug_struct("Progress").field("id", id
).field("progress", progress
).finish()
114 DidCheckCrate(String
),
115 DidFinish(io
::Result
<()>),
121 struct FlycheckActor
{
123 sender
: Box
<dyn Fn(Message
) + Send
>,
124 config
: FlycheckConfig
,
125 workspace_root
: AbsPathBuf
,
126 /// CargoHandle exists to wrap around the communication needed to be able to
127 /// run `cargo check` without blocking. Currently the Rust standard library
128 /// doesn't provide a way to read sub-process output without blocking, so we
129 /// have to wrap sub-processes output handling in a thread and pass messages
130 /// back over a channel.
131 cargo_handle
: Option
<CargoHandle
>,
136 CheckEvent(Option
<CargoMessage
>),
142 sender
: Box
<dyn Fn(Message
) + Send
>,
143 config
: FlycheckConfig
,
144 workspace_root
: AbsPathBuf
,
146 FlycheckActor { id, sender, config, workspace_root, cargo_handle: None }
148 fn progress(&self, progress
: Progress
) {
149 self.send(Message
::Progress { id: self.id, progress }
);
151 fn next_event(&self, inbox
: &Receiver
<Restart
>) -> Option
<Event
> {
152 let check_chan
= self.cargo_handle
.as_ref().map(|cargo
| &cargo
.receiver
);
154 recv(inbox
) -> msg
=> msg
.ok().map(Event
::Restart
),
155 recv(check_chan
.unwrap_or(&never())) -> msg
=> Some(Event
::CheckEvent(msg
.ok())),
158 fn run(mut self, inbox
: Receiver
<Restart
>) {
159 while let Some(event
) = self.next_event(&inbox
) {
161 Event
::Restart(Restart
) => {
162 // Cancel the previously spawned process
163 self.cancel_check_process();
164 while let Ok(Restart
) = inbox
.recv_timeout(Duration
::from_millis(50)) {}
166 let command
= self.check_command();
167 tracing
::debug
!(?command
, "will restart flycheck");
168 match CargoHandle
::spawn(command
) {
169 Ok(cargo_handle
) => {
171 command
= ?
self.check_command(),
172 "did restart flycheck"
174 self.cargo_handle
= Some(cargo_handle
);
175 self.progress(Progress
::DidStart
);
179 command
= ?
self.check_command(),
180 %error
, "failed to restart flycheck"
185 Event
::CheckEvent(None
) => {
186 tracing
::debug
!("flycheck finished");
189 let cargo_handle
= self.cargo_handle
.take().unwrap();
190 let res
= cargo_handle
.join();
193 "Flycheck failed to run the following command: {:?}",
197 self.progress(Progress
::DidFinish(res
));
199 Event
::CheckEvent(Some(message
)) => match message
{
200 CargoMessage
::CompilerArtifact(msg
) => {
201 self.progress(Progress
::DidCheckCrate(msg
.target
.name
));
204 CargoMessage
::Diagnostic(msg
) => {
205 self.send(Message
::AddDiagnostic
{
206 workspace_root
: self.workspace_root
.clone(),
213 // If we rerun the thread, we need to discard the previous check results first
214 self.cancel_check_process();
217 fn cancel_check_process(&mut self) {
218 if let Some(cargo_handle
) = self.cargo_handle
.take() {
219 cargo_handle
.cancel();
220 self.progress(Progress
::DidCancel
);
224 fn check_command(&self) -> Command
{
225 let mut cmd
= match &self.config
{
226 FlycheckConfig
::CargoCommand
{
235 let mut cmd
= Command
::new(toolchain
::cargo());
237 cmd
.current_dir(&self.workspace_root
);
238 cmd
.args(&["--workspace", "--message-format=json", "--manifest-path"])
239 .arg(self.workspace_root
.join("Cargo.toml").as_os_str());
241 if let Some(target
) = target_triple
{
242 cmd
.args(&["--target", target
.as_str()]);
245 cmd
.arg("--all-targets");
248 cmd
.arg("--all-features");
250 if *no_default_features
{
251 cmd
.arg("--no-default-features");
253 if !features
.is_empty() {
254 cmd
.arg("--features");
255 cmd
.arg(features
.join(" "));
258 cmd
.args(extra_args
);
261 FlycheckConfig
::CustomCommand { command, args }
=> {
262 let mut cmd
= Command
::new(command
);
267 cmd
.current_dir(&self.workspace_root
);
271 fn send(&self, check_task
: Message
) {
272 (self.sender
)(check_task
);
276 /// A handle to a cargo process used for fly-checking.
278 /// The handle to the actual cargo process. As we cannot cancel directly from with
279 /// a read syscall dropping and therefor terminating the process is our best option.
281 thread
: jod_thread
::JoinHandle
<io
::Result
<(bool
, String
)>>,
282 receiver
: Receiver
<CargoMessage
>,
286 fn spawn(mut command
: Command
) -> std
::io
::Result
<CargoHandle
> {
287 command
.stdout(Stdio
::piped()).stderr(Stdio
::piped()).stdin(Stdio
::null());
288 let mut child
= JodChild
::spawn(command
)?
;
290 let stdout
= child
.stdout
.take().unwrap();
291 let stderr
= child
.stderr
.take().unwrap();
293 let (sender
, receiver
) = unbounded();
294 let actor
= CargoActor
::new(sender
, stdout
, stderr
);
295 let thread
= jod_thread
::Builder
::new()
296 .name("CargoHandle".to_owned())
297 .spawn(move || actor
.run())
298 .expect("failed to spawn thread");
299 Ok(CargoHandle { child, thread, receiver }
)
302 fn cancel(mut self) {
303 let _
= self.child
.kill();
304 let _
= self.child
.wait();
307 fn join(mut self) -> io
::Result
<()> {
308 let _
= self.child
.kill();
309 let exit_status
= self.child
.wait()?
;
310 let (read_at_least_one_message
, error
) = self.thread
.join()?
;
311 if read_at_least_one_message
|| exit_status
.success() {
314 Err(io
::Error
::new(io
::ErrorKind
::Other
, format
!(
315 "Cargo watcher failed, the command produced no valid metadata (exit code: {:?}):\n{}",
323 sender
: Sender
<CargoMessage
>,
329 fn new(sender
: Sender
<CargoMessage
>, stdout
: ChildStdout
, stderr
: ChildStderr
) -> CargoActor
{
330 CargoActor { sender, stdout, stderr }
333 fn run(self) -> io
::Result
<(bool
, String
)> {
334 // We manually read a line at a time, instead of using serde's
335 // stream deserializers, because the deserializer cannot recover
336 // from an error, resulting in it getting stuck, because we try to
337 // be resilient against failures.
339 // Because cargo only outputs one JSON object per line, we can
340 // simply skip a line if it doesn't parse, which just ignores any
343 let mut error
= String
::new();
344 let mut read_at_least_one_message
= false;
345 let output
= streaming_output(
349 read_at_least_one_message
= true;
351 // Try to deserialize a message from Cargo or Rustc.
352 let mut deserializer
= serde_json
::Deserializer
::from_str(line
);
353 deserializer
.disable_recursion_limit();
354 if let Ok(message
) = JsonMessage
::deserialize(&mut deserializer
) {
356 // Skip certain kinds of messages to only spend time on what's useful
357 JsonMessage
::Cargo(message
) => match message
{
358 cargo_metadata
::Message
::CompilerArtifact(artifact
)
359 if !artifact
.fresh
=>
361 self.sender
.send(CargoMessage
::CompilerArtifact(artifact
)).unwrap();
363 cargo_metadata
::Message
::CompilerMessage(msg
) => {
364 self.sender
.send(CargoMessage
::Diagnostic(msg
.message
)).unwrap();
368 JsonMessage
::Rustc(message
) => {
369 self.sender
.send(CargoMessage
::Diagnostic(message
)).unwrap();
375 error
.push_str(line
);
380 Ok(_
) => Ok((read_at_least_one_message
, error
)),
381 Err(e
) => Err(io
::Error
::new(e
.kind(), format
!("{:?}: {}", e
, error
))),
387 CompilerArtifact(cargo_metadata
::Artifact
),
388 Diagnostic(Diagnostic
),
391 #[derive(Deserialize)]
394 Cargo(cargo_metadata
::Message
),