1use std::{fmt, io, process::Command, time::Duration};
10
11use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
12use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
13use rustc_hash::FxHashMap;
14use serde::Deserialize;
15
16pub use cargo_metadata::diagnostic::{
17 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
18 DiagnosticSpanMacroExpansion,
19};
20use toolchain::Tool;
21
22mod command;
23pub mod project_json;
24mod test_runner;
25
26use command::{CommandHandle, ParseFromLine};
27pub use test_runner::{CargoTestHandle, CargoTestMessage, TestState, TestTarget};
28
29#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
30pub enum InvocationStrategy {
31 Once,
32 #[default]
33 PerWorkspace,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub enum InvocationLocation {
38 Root(AbsPathBuf),
39 #[default]
40 Workspace,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct CargoOptions {
45 pub target_triples: Vec<String>,
46 pub all_targets: bool,
47 pub no_default_features: bool,
48 pub all_features: bool,
49 pub features: Vec<String>,
50 pub extra_args: Vec<String>,
51 pub extra_env: FxHashMap<String, String>,
52 pub target_dir: Option<Utf8PathBuf>,
53}
54
55impl CargoOptions {
56 fn apply_on_command(&self, cmd: &mut Command) {
57 for target in &self.target_triples {
58 cmd.args(["--target", target.as_str()]);
59 }
60 if self.all_targets {
61 cmd.arg("--all-targets");
62 }
63 if self.all_features {
64 cmd.arg("--all-features");
65 } else {
66 if self.no_default_features {
67 cmd.arg("--no-default-features");
68 }
69 if !self.features.is_empty() {
70 cmd.arg("--features");
71 cmd.arg(self.features.join(" "));
72 }
73 }
74 if let Some(target_dir) = &self.target_dir {
75 cmd.arg("--target-dir").arg(target_dir);
76 }
77 cmd.envs(&self.extra_env);
78 }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum FlycheckConfig {
83 CargoCommand {
84 command: String,
85 options: CargoOptions,
86 ansi_color_output: bool,
87 },
88 CustomCommand {
89 command: String,
90 args: Vec<String>,
91 extra_env: FxHashMap<String, String>,
92 invocation_strategy: InvocationStrategy,
93 invocation_location: InvocationLocation,
94 },
95}
96
97impl fmt::Display for FlycheckConfig {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 match self {
100 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
101 FlycheckConfig::CustomCommand { command, args, .. } => {
102 write!(f, "{command} {}", args.join(" "))
103 }
104 }
105 }
106}
107
108#[derive(Debug)]
113pub struct FlycheckHandle {
114 sender: Sender<StateChange>,
116 _thread: stdx::thread::JoinHandle,
117 id: usize,
118}
119
120impl FlycheckHandle {
121 pub fn spawn(
122 id: usize,
123 sender: Box<dyn Fn(Message) + Send>,
124 config: FlycheckConfig,
125 sysroot_root: Option<AbsPathBuf>,
126 workspace_root: AbsPathBuf,
127 manifest_path: Option<AbsPathBuf>,
128 ) -> FlycheckHandle {
129 let actor =
130 FlycheckActor::new(id, sender, config, sysroot_root, workspace_root, manifest_path);
131 let (sender, receiver) = unbounded::<StateChange>();
132 let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
133 .name("Flycheck".to_owned())
134 .spawn(move || actor.run(receiver))
135 .expect("failed to spawn thread");
136 FlycheckHandle { id, sender, _thread: thread }
137 }
138
139 pub fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
141 self.sender.send(StateChange::Restart { package: None, saved_file }).unwrap();
142 }
143
144 pub fn restart_for_package(&self, package: String) {
146 self.sender
147 .send(StateChange::Restart { package: Some(package), saved_file: None })
148 .unwrap();
149 }
150
151 pub fn cancel(&self) {
153 self.sender.send(StateChange::Cancel).unwrap();
154 }
155
156 pub fn id(&self) -> usize {
157 self.id
158 }
159}
160
161pub enum Message {
162 AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
164
165 ClearDiagnostics { id: usize },
167
168 Progress {
170 id: usize,
172 progress: Progress,
173 },
174}
175
176impl fmt::Debug for Message {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 Message::AddDiagnostic { id, workspace_root, diagnostic } => f
180 .debug_struct("AddDiagnostic")
181 .field("id", id)
182 .field("workspace_root", workspace_root)
183 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
184 .finish(),
185 Message::ClearDiagnostics { id } => {
186 f.debug_struct("ClearDiagnostics").field("id", id).finish()
187 }
188 Message::Progress { id, progress } => {
189 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
190 }
191 }
192 }
193}
194
195#[derive(Debug)]
196pub enum Progress {
197 DidStart,
198 DidCheckCrate(String),
199 DidFinish(io::Result<()>),
200 DidCancel,
201 DidFailToRestart(String),
202}
203
204enum StateChange {
205 Restart { package: Option<String>, saved_file: Option<AbsPathBuf> },
206 Cancel,
207}
208
209struct FlycheckActor {
211 id: usize,
213 sender: Box<dyn Fn(Message) + Send>,
214 config: FlycheckConfig,
215 manifest_path: Option<AbsPathBuf>,
216 root: AbsPathBuf,
219 sysroot_root: Option<AbsPathBuf>,
220 command_handle: Option<CommandHandle<CargoCheckMessage>>,
226 command_receiver: Option<Receiver<CargoCheckMessage>>,
228
229 status: FlycheckStatus,
230}
231
232enum Event {
233 RequestStateChange(StateChange),
234 CheckEvent(Option<CargoCheckMessage>),
235}
236
237#[derive(PartialEq)]
238enum FlycheckStatus {
239 Started,
240 DiagnosticSent,
241 Finished,
242}
243
244pub const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
245
246impl FlycheckActor {
247 fn new(
248 id: usize,
249 sender: Box<dyn Fn(Message) + Send>,
250 config: FlycheckConfig,
251 sysroot_root: Option<AbsPathBuf>,
252 workspace_root: AbsPathBuf,
253 manifest_path: Option<AbsPathBuf>,
254 ) -> FlycheckActor {
255 tracing::info!(%id, ?workspace_root, "Spawning flycheck");
256 FlycheckActor {
257 id,
258 sender,
259 config,
260 sysroot_root,
261 root: workspace_root,
262 manifest_path,
263 command_handle: None,
264 command_receiver: None,
265 status: FlycheckStatus::Finished,
266 }
267 }
268
269 fn report_progress(&self, progress: Progress) {
270 self.send(Message::Progress { id: self.id, progress });
271 }
272
273 fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
274 if let Ok(msg) = inbox.try_recv() {
275 return Some(Event::RequestStateChange(msg));
277 }
278 select! {
279 recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
280 recv(self.command_receiver.as_ref().unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
281 }
282 }
283
284 fn run(mut self, inbox: Receiver<StateChange>) {
285 'event: while let Some(event) = self.next_event(&inbox) {
286 match event {
287 Event::RequestStateChange(StateChange::Cancel) => {
288 tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
289 self.cancel_check_process();
290 }
291 Event::RequestStateChange(StateChange::Restart { package, saved_file }) => {
292 self.cancel_check_process();
294 while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
295 if let StateChange::Cancel = restart {
297 continue 'event;
298 }
299 }
300
301 let command =
302 match self.check_command(package.as_deref(), saved_file.as_deref()) {
303 Some(c) => c,
304 None => continue,
305 };
306 let formatted_command = format!("{command:?}");
307
308 tracing::debug!(?command, "will restart flycheck");
309 let (sender, receiver) = unbounded();
310 match CommandHandle::spawn(command, sender) {
311 Ok(command_handle) => {
312 tracing::debug!(command = formatted_command, "did restart flycheck");
313 self.command_handle = Some(command_handle);
314 self.command_receiver = Some(receiver);
315 self.report_progress(Progress::DidStart);
316 self.status = FlycheckStatus::Started;
317 }
318 Err(error) => {
319 self.report_progress(Progress::DidFailToRestart(format!(
320 "Failed to run the following command: {formatted_command} error={error}"
321 )));
322 self.status = FlycheckStatus::Finished;
323 }
324 }
325 }
326 Event::CheckEvent(None) => {
327 tracing::debug!(flycheck_id = self.id, "flycheck finished");
328
329 let command_handle = self.command_handle.take().unwrap();
331 self.command_receiver.take();
332 let formatted_handle = format!("{command_handle:?}");
333
334 let res = command_handle.join();
335 if let Err(error) = &res {
336 tracing::error!(
337 "Flycheck failed to run the following command: {}, error={}",
338 formatted_handle,
339 error
340 );
341 }
342 if self.status == FlycheckStatus::Started {
343 self.send(Message::ClearDiagnostics { id: self.id });
344 }
345 self.report_progress(Progress::DidFinish(res));
346 self.status = FlycheckStatus::Finished;
347 }
348 Event::CheckEvent(Some(message)) => match message {
349 CargoCheckMessage::CompilerArtifact(msg) => {
350 tracing::trace!(
351 flycheck_id = self.id,
352 artifact = msg.target.name,
353 "artifact received"
354 );
355 self.report_progress(Progress::DidCheckCrate(msg.target.name));
356 }
357
358 CargoCheckMessage::Diagnostic(msg) => {
359 tracing::trace!(
360 flycheck_id = self.id,
361 message = msg.message,
362 "diagnostic received"
363 );
364 if self.status == FlycheckStatus::Started {
365 self.send(Message::ClearDiagnostics { id: self.id });
366 }
367 self.send(Message::AddDiagnostic {
368 id: self.id,
369 workspace_root: self.root.clone(),
370 diagnostic: msg,
371 });
372 self.status = FlycheckStatus::DiagnosticSent;
373 }
374 },
375 }
376 }
377 self.cancel_check_process();
379 }
380
381 fn cancel_check_process(&mut self) {
382 if let Some(command_handle) = self.command_handle.take() {
383 tracing::debug!(
384 command = ?command_handle,
385 "did cancel flycheck"
386 );
387 command_handle.cancel();
388 self.command_receiver.take();
389 self.report_progress(Progress::DidCancel);
390 self.status = FlycheckStatus::Finished;
391 }
392 }
393
394 fn check_command(
398 &self,
399 package: Option<&str>,
400 saved_file: Option<&AbsPath>,
401 ) -> Option<Command> {
402 let (mut cmd, args) = match &self.config {
403 FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
404 let mut cmd = Command::new(Tool::Cargo.path());
405 if let Some(sysroot_root) = &self.sysroot_root {
406 cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
407 }
408 cmd.arg(command);
409 cmd.current_dir(&self.root);
410
411 match package {
412 Some(pkg) => cmd.arg("-p").arg(pkg),
413 None => cmd.arg("--workspace"),
414 };
415
416 cmd.arg(if *ansi_color_output {
417 "--message-format=json-diagnostic-rendered-ansi"
418 } else {
419 "--message-format=json"
420 });
421
422 if let Some(manifest_path) = &self.manifest_path {
423 cmd.arg("--manifest-path");
424 cmd.arg(manifest_path);
425 if manifest_path.extension().map_or(false, |ext| ext == "rs") {
426 cmd.arg("-Zscript");
427 }
428 }
429
430 cmd.arg("--keep-going");
431
432 options.apply_on_command(&mut cmd);
433 (cmd, options.extra_args.clone())
434 }
435 FlycheckConfig::CustomCommand {
436 command,
437 args,
438 extra_env,
439 invocation_strategy,
440 invocation_location,
441 } => {
442 let mut cmd = Command::new(command);
443 cmd.envs(extra_env);
444
445 match invocation_location {
446 InvocationLocation::Workspace => {
447 match invocation_strategy {
448 InvocationStrategy::Once => {
449 cmd.current_dir(&self.root);
450 }
451 InvocationStrategy::PerWorkspace => {
452 cmd.current_dir(&self.root);
454 }
455 }
456 }
457 InvocationLocation::Root(root) => {
458 cmd.current_dir(root);
459 }
460 }
461
462 if args.contains(&SAVED_FILE_PLACEHOLDER.to_owned()) {
463 if let Some(saved_file) = saved_file {
466 let args = args
467 .iter()
468 .map(|arg| {
469 if arg == SAVED_FILE_PLACEHOLDER {
470 saved_file.to_string()
471 } else {
472 arg.clone()
473 }
474 })
475 .collect();
476 (cmd, args)
477 } else {
478 return None;
481 }
482 } else {
483 (cmd, args.clone())
484 }
485 }
486 };
487
488 cmd.args(args);
489 Some(cmd)
490 }
491
492 fn send(&self, check_task: Message) {
493 (self.sender)(check_task);
494 }
495}
496
497#[allow(clippy::large_enum_variant)]
498enum CargoCheckMessage {
499 CompilerArtifact(cargo_metadata::Artifact),
500 Diagnostic(Diagnostic),
501}
502
503impl ParseFromLine for CargoCheckMessage {
504 fn from_line(line: &str, error: &mut String) -> Option<Self> {
505 let mut deserializer = serde_json::Deserializer::from_str(line);
506 deserializer.disable_recursion_limit();
507 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
508 return match message {
509 JsonMessage::Cargo(message) => match message {
511 cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
512 Some(CargoCheckMessage::CompilerArtifact(artifact))
513 }
514 cargo_metadata::Message::CompilerMessage(msg) => {
515 Some(CargoCheckMessage::Diagnostic(msg.message))
516 }
517 _ => None,
518 },
519 JsonMessage::Rustc(message) => Some(CargoCheckMessage::Diagnostic(message)),
520 };
521 }
522
523 error.push_str(line);
524 error.push('\n');
525 None
526 }
527
528 fn from_eof() -> Option<Self> {
529 None
530 }
531}
532
533#[derive(Deserialize)]
534#[serde(untagged)]
535enum JsonMessage {
536 Cargo(cargo_metadata::Message),
537 Rustc(Diagnostic),
538}