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 // FIXME: This crate now handles running `cargo test` needed in the test explorer in
6 // addition to `cargo check`. Either split it into 3 crates (one for test, one for check
7 // and one common utilities) or change its name and docs to reflect the current state.
9 #![warn(rust_2018_idioms, unused_lifetimes)]
11 use std
::{fmt, io, process::Command, time::Duration}
;
13 use crossbeam_channel
::{never, select, unbounded, Receiver, Sender}
;
14 use paths
::{AbsPath, AbsPathBuf, Utf8PathBuf}
;
15 use rustc_hash
::FxHashMap
;
16 use serde
::Deserialize
;
18 pub use cargo_metadata
::diagnostic
::{
19 Applicability
, Diagnostic
, DiagnosticCode
, DiagnosticLevel
, DiagnosticSpan
,
20 DiagnosticSpanMacroExpansion
,
27 use command
::{CommandHandle, ParseFromLine}
;
28 pub use test_runner
::{CargoTestHandle, CargoTestMessage, TestState}
;
30 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
31 pub enum InvocationStrategy
{
37 #[derive(Clone, Debug, Default, PartialEq, Eq)]
38 pub enum InvocationLocation
{
44 #[derive(Clone, Debug, PartialEq, Eq)]
45 pub struct CargoOptions
{
46 pub target_triples
: Vec
<String
>,
47 pub all_targets
: bool
,
48 pub no_default_features
: bool
,
49 pub all_features
: bool
,
50 pub features
: Vec
<String
>,
51 pub extra_args
: Vec
<String
>,
52 pub extra_env
: FxHashMap
<String
, String
>,
53 pub target_dir
: Option
<Utf8PathBuf
>,
57 fn apply_on_command(&self, cmd
: &mut Command
) {
58 for target
in &self.target_triples
{
59 cmd
.args(["--target", target
.as_str()]);
62 cmd
.arg("--all-targets");
64 if self.all_features
{
65 cmd
.arg("--all-features");
67 if self.no_default_features
{
68 cmd
.arg("--no-default-features");
70 if !self.features
.is_empty() {
71 cmd
.arg("--features");
72 cmd
.arg(self.features
.join(" "));
75 if let Some(target_dir
) = &self.target_dir
{
76 cmd
.arg("--target-dir").arg(target_dir
);
78 cmd
.envs(&self.extra_env
);
82 #[derive(Clone, Debug, PartialEq, Eq)]
83 pub enum FlycheckConfig
{
86 options
: CargoOptions
,
87 ansi_color_output
: bool
,
92 extra_env
: FxHashMap
<String
, String
>,
93 invocation_strategy
: InvocationStrategy
,
94 invocation_location
: InvocationLocation
,
98 impl fmt
::Display
for FlycheckConfig
{
99 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
101 FlycheckConfig
::CargoCommand { command, .. }
=> write
!(f
, "cargo {command}"),
102 FlycheckConfig
::CustomCommand { command, args, .. }
=> {
103 write
!(f
, "{command} {}", args
.join(" "))
109 /// Flycheck wraps the shared state and communication machinery used for
110 /// running `cargo check` (or other compatible command) and providing
111 /// diagnostics based on the output.
112 /// The spawned thread is shut down when this struct is dropped.
114 pub struct FlycheckHandle
{
115 // XXX: drop order is significant
116 sender
: Sender
<StateChange
>,
117 _thread
: stdx
::thread
::JoinHandle
,
121 impl FlycheckHandle
{
124 sender
: Box
<dyn Fn(Message
) + Send
>,
125 config
: FlycheckConfig
,
126 sysroot_root
: Option
<AbsPathBuf
>,
127 workspace_root
: AbsPathBuf
,
128 manifest_path
: Option
<AbsPathBuf
>,
129 ) -> FlycheckHandle
{
131 FlycheckActor
::new(id
, sender
, config
, sysroot_root
, workspace_root
, manifest_path
);
132 let (sender
, receiver
) = unbounded
::<StateChange
>();
133 let thread
= stdx
::thread
::Builder
::new(stdx
::thread
::ThreadIntent
::Worker
)
134 .name("Flycheck".to_owned())
135 .spawn(move || actor
.run(receiver
))
136 .expect("failed to spawn thread");
137 FlycheckHandle { id, sender, _thread: thread }
140 /// Schedule a re-start of the cargo check worker to do a workspace wide check.
141 pub fn restart_workspace(&self, saved_file
: Option
<AbsPathBuf
>) {
142 self.sender
.send(StateChange
::Restart { package: None, saved_file }
).unwrap();
145 /// Schedule a re-start of the cargo check worker to do a package wide check.
146 pub fn restart_for_package(&self, package
: String
) {
148 .send(StateChange
::Restart { package: Some(package), saved_file: None }
)
152 /// Stop this cargo check worker.
153 pub fn cancel(&self) {
154 self.sender
.send(StateChange
::Cancel
).unwrap();
157 pub fn id(&self) -> usize {
163 /// Request adding a diagnostic with fixes included to a file
164 AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic }
,
166 /// Request clearing all previous diagnostics
167 ClearDiagnostics { id: usize }
,
169 /// Request check progress notification to client
171 /// Flycheck instance ID
177 impl fmt
::Debug
for Message
{
178 fn fmt(&self, f
: &mut fmt
::Formatter
<'_
>) -> fmt
::Result
{
180 Message
::AddDiagnostic { id, workspace_root, diagnostic }
=> f
181 .debug_struct("AddDiagnostic")
183 .field("workspace_root", workspace_root
)
184 .field("diagnostic_code", &diagnostic
.code
.as_ref().map(|it
| &it
.code
))
186 Message
::ClearDiagnostics { id }
=> {
187 f
.debug_struct("ClearDiagnostics").field("id", id
).finish()
189 Message
::Progress { id, progress }
=> {
190 f
.debug_struct("Progress").field("id", id
).field("progress", progress
).finish()
199 DidCheckCrate(String
),
200 DidFinish(io
::Result
<()>),
202 DidFailToRestart(String
),
206 Restart { package: Option<String>, saved_file: Option<AbsPathBuf> }
,
210 /// A [`FlycheckActor`] is a single check instance of a workspace.
211 struct FlycheckActor
{
212 /// The workspace id of this flycheck instance.
214 sender
: Box
<dyn Fn(Message
) + Send
>,
215 config
: FlycheckConfig
,
216 manifest_path
: Option
<AbsPathBuf
>,
217 /// Either the workspace root of the workspace we are flychecking,
218 /// or the project root of the project.
220 sysroot_root
: Option
<AbsPathBuf
>,
221 /// CargoHandle exists to wrap around the communication needed to be able to
222 /// run `cargo check` without blocking. Currently the Rust standard library
223 /// doesn't provide a way to read sub-process output without blocking, so we
224 /// have to wrap sub-processes output handling in a thread and pass messages
225 /// back over a channel.
226 command_handle
: Option
<CommandHandle
<CargoCheckMessage
>>,
227 /// The receiver side of the channel mentioned above.
228 command_receiver
: Option
<Receiver
<CargoCheckMessage
>>,
230 status
: FlycheckStatus
,
234 RequestStateChange(StateChange
),
235 CheckEvent(Option
<CargoCheckMessage
>),
239 enum FlycheckStatus
{
245 const SAVED_FILE_PLACEHOLDER
: &str = "$saved_file";
250 sender
: Box
<dyn Fn(Message
) + Send
>,
251 config
: FlycheckConfig
,
252 sysroot_root
: Option
<AbsPathBuf
>,
253 workspace_root
: AbsPathBuf
,
254 manifest_path
: Option
<AbsPathBuf
>,
256 tracing
::info
!(%id
, ?workspace_root
, "Spawning flycheck");
262 root
: workspace_root
,
264 command_handle
: None
,
265 command_receiver
: None
,
266 status
: FlycheckStatus
::Finished
,
270 fn report_progress(&self, progress
: Progress
) {
271 self.send(Message
::Progress { id: self.id, progress }
);
274 fn next_event(&self, inbox
: &Receiver
<StateChange
>) -> Option
<Event
> {
275 if let Ok(msg
) = inbox
.try_recv() {
276 // give restarts a preference so check outputs don't block a restart or stop
277 return Some(Event
::RequestStateChange(msg
));
280 recv(inbox
) -> msg
=> msg
.ok().map(Event
::RequestStateChange
),
281 recv(self.command_receiver
.as_ref().unwrap_or(&never())) -> msg
=> Some(Event
::CheckEvent(msg
.ok())),
285 fn run(mut self, inbox
: Receiver
<StateChange
>) {
286 'event
: while let Some(event
) = self.next_event(&inbox
) {
288 Event
::RequestStateChange(StateChange
::Cancel
) => {
289 tracing
::debug
!(flycheck_id
= self.id
, "flycheck cancelled");
290 self.cancel_check_process();
292 Event
::RequestStateChange(StateChange
::Restart { package, saved_file }
) => {
293 // Cancel the previously spawned process
294 self.cancel_check_process();
295 while let Ok(restart
) = inbox
.recv_timeout(Duration
::from_millis(50)) {
296 // restart chained with a stop, so just cancel
297 if let StateChange
::Cancel
= restart
{
303 match self.check_command(package
.as_deref(), saved_file
.as_deref()) {
307 let formatted_command
= format
!("{:?}", command
);
309 tracing
::debug
!(?command
, "will restart flycheck");
310 let (sender
, receiver
) = unbounded();
311 match CommandHandle
::spawn(command
, sender
) {
312 Ok(command_handle
) => {
313 tracing
::debug
!(command
= formatted_command
, "did restart flycheck");
314 self.command_handle
= Some(command_handle
);
315 self.command_receiver
= Some(receiver
);
316 self.report_progress(Progress
::DidStart
);
317 self.status
= FlycheckStatus
::Started
;
320 self.report_progress(Progress
::DidFailToRestart(format
!(
321 "Failed to run the following command: {} error={}",
322 formatted_command
, error
324 self.status
= FlycheckStatus
::Finished
;
328 Event
::CheckEvent(None
) => {
329 tracing
::debug
!(flycheck_id
= self.id
, "flycheck finished");
332 let command_handle
= self.command_handle
.take().unwrap();
333 self.command_receiver
.take();
334 let formatted_handle
= format
!("{:?}", command_handle
);
336 let res
= command_handle
.join();
337 if let Err(error
) = &res
{
339 "Flycheck failed to run the following command: {}, error={}",
344 if self.status
== FlycheckStatus
::Started
{
345 self.send(Message
::ClearDiagnostics { id: self.id }
);
347 self.report_progress(Progress
::DidFinish(res
));
348 self.status
= FlycheckStatus
::Finished
;
350 Event
::CheckEvent(Some(message
)) => match message
{
351 CargoCheckMessage
::CompilerArtifact(msg
) => {
353 flycheck_id
= self.id
,
354 artifact
= msg
.target
.name
,
357 self.report_progress(Progress
::DidCheckCrate(msg
.target
.name
));
360 CargoCheckMessage
::Diagnostic(msg
) => {
362 flycheck_id
= self.id
,
363 message
= msg
.message
,
364 "diagnostic received"
366 if self.status
== FlycheckStatus
::Started
{
367 self.send(Message
::ClearDiagnostics { id: self.id }
);
369 self.send(Message
::AddDiagnostic
{
371 workspace_root
: self.root
.clone(),
374 self.status
= FlycheckStatus
::DiagnosticSent
;
379 // If we rerun the thread, we need to discard the previous check results first
380 self.cancel_check_process();
383 fn cancel_check_process(&mut self) {
384 if let Some(command_handle
) = self.command_handle
.take() {
386 command
= ?command_handle
,
387 "did cancel flycheck"
389 command_handle
.cancel();
390 self.report_progress(Progress
::DidCancel
);
391 self.status
= FlycheckStatus
::Finished
;
395 /// Construct a `Command` object for checking the user's code. If the user
396 /// has specified a custom command with placeholders that we cannot fill,
400 package
: Option
<&str>,
401 saved_file
: Option
<&AbsPath
>,
402 ) -> Option
<Command
> {
403 let (mut cmd
, args
) = match &self.config
{
404 FlycheckConfig
::CargoCommand { command, options, ansi_color_output }
=> {
405 let mut cmd
= Command
::new(Tool
::Cargo
.path());
406 if let Some(sysroot_root
) = &self.sysroot_root
{
407 cmd
.env("RUSTUP_TOOLCHAIN", AsRef
::<std
::path
::Path
>::as_ref(sysroot_root
));
410 cmd
.current_dir(&self.root
);
413 Some(pkg
) => cmd
.arg("-p").arg(pkg
),
414 None
=> cmd
.arg("--workspace"),
417 cmd
.arg(if *ansi_color_output
{
418 "--message-format=json-diagnostic-rendered-ansi"
420 "--message-format=json"
423 if let Some(manifest_path
) = &self.manifest_path
{
424 cmd
.arg("--manifest-path");
425 cmd
.arg(manifest_path
);
426 if manifest_path
.extension().map_or(false, |ext
| ext
== "rs") {
431 options
.apply_on_command(&mut cmd
);
432 (cmd
, options
.extra_args
.clone())
434 FlycheckConfig
::CustomCommand
{
441 let mut cmd
= Command
::new(command
);
444 match invocation_location
{
445 InvocationLocation
::Workspace
=> {
446 match invocation_strategy
{
447 InvocationStrategy
::Once
=> {
448 cmd
.current_dir(&self.root
);
450 InvocationStrategy
::PerWorkspace
=> {
451 // FIXME: cmd.current_dir(&affected_workspace);
452 cmd
.current_dir(&self.root
);
456 InvocationLocation
::Root(root
) => {
457 cmd
.current_dir(root
);
461 if args
.contains(&SAVED_FILE_PLACEHOLDER
.to_owned()) {
462 // If the custom command has a $saved_file placeholder, and
463 // we're saving a file, replace the placeholder in the arguments.
464 if let Some(saved_file
) = saved_file
{
468 if arg
== SAVED_FILE_PLACEHOLDER
{
469 saved_file
.to_string()
477 // The custom command has a $saved_file placeholder,
478 // but we had an IDE event that wasn't a file save. Do nothing.
491 fn send(&self, check_task
: Message
) {
492 (self.sender
)(check_task
);
496 #[allow(clippy::large_enum_variant)]
497 enum CargoCheckMessage
{
498 CompilerArtifact(cargo_metadata
::Artifact
),
499 Diagnostic(Diagnostic
),
502 impl ParseFromLine
for CargoCheckMessage
{
503 fn from_line(line
: &str, error
: &mut String
) -> Option
<Self> {
504 let mut deserializer
= serde_json
::Deserializer
::from_str(line
);
505 deserializer
.disable_recursion_limit();
506 if let Ok(message
) = JsonMessage
::deserialize(&mut deserializer
) {
507 return match message
{
508 // Skip certain kinds of messages to only spend time on what's useful
509 JsonMessage
::Cargo(message
) => match message
{
510 cargo_metadata
::Message
::CompilerArtifact(artifact
) if !artifact
.fresh
=> {
511 Some(CargoCheckMessage
::CompilerArtifact(artifact
))
513 cargo_metadata
::Message
::CompilerMessage(msg
) => {
514 Some(CargoCheckMessage
::Diagnostic(msg
.message
))
518 JsonMessage
::Rustc(message
) => Some(CargoCheckMessage
::Diagnostic(message
)),
522 error
.push_str(line
);
527 fn from_eof() -> Option
<Self> {
532 #[derive(Deserialize)]
535 Cargo(cargo_metadata
::Message
),