1use std::{
5 fmt, io,
6 process::Command,
7 sync::atomic::{AtomicUsize, Ordering},
8 time::Duration,
9};
10
11use cargo_metadata::PackageId;
12use crossbeam_channel::{Receiver, Sender, select_biased, unbounded};
13use ide_db::FxHashSet;
14use itertools::Itertools;
15use paths::{AbsPath, AbsPathBuf, Utf8Path, Utf8PathBuf};
16use project_model::TargetDirectoryConfig;
17use rustc_hash::FxHashMap;
18use serde::Deserialize as _;
19use serde_derive::Deserialize;
20
21pub(crate) use cargo_metadata::diagnostic::{
22 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
23};
24use toolchain::Tool;
25use triomphe::Arc;
26
27use crate::{
28 command::{CommandHandle, JsonLinesParser},
29 diagnostics::DiagnosticsGeneration,
30};
31
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
33pub(crate) enum InvocationStrategy {
34 Once,
35 #[default]
36 PerWorkspace,
37}
38
39#[derive(Clone, Debug, PartialEq, Eq)]
40pub(crate) struct CargoOptions {
41 pub(crate) target_tuples: Vec<String>,
42 pub(crate) all_targets: bool,
43 pub(crate) set_test: bool,
44 pub(crate) no_default_features: bool,
45 pub(crate) all_features: bool,
46 pub(crate) features: Vec<String>,
47 pub(crate) extra_args: Vec<String>,
48 pub(crate) extra_test_bin_args: Vec<String>,
49 pub(crate) extra_env: FxHashMap<String, Option<String>>,
50 pub(crate) target_dir_config: TargetDirectoryConfig,
51}
52
53#[derive(Clone, Debug)]
54pub(crate) enum Target {
55 Bin(String),
56 Example(String),
57 Benchmark(String),
58 Test(String),
59}
60
61impl CargoOptions {
62 pub(crate) fn apply_on_command(&self, cmd: &mut Command, ws_target_dir: Option<&Utf8Path>) {
63 for target in &self.target_tuples {
64 cmd.args(["--target", target.as_str()]);
65 }
66 if self.all_targets {
67 if self.set_test {
68 cmd.arg("--all-targets");
69 } else {
70 cmd.args(["--lib", "--bins", "--examples"]);
73 }
74 }
75 if self.all_features {
76 cmd.arg("--all-features");
77 } else {
78 if self.no_default_features {
79 cmd.arg("--no-default-features");
80 }
81 if !self.features.is_empty() {
82 cmd.arg("--features");
83 cmd.arg(self.features.join(" "));
84 }
85 }
86 if let Some(target_dir) = self.target_dir_config.target_dir(ws_target_dir) {
87 cmd.arg("--target-dir").arg(target_dir.as_ref());
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub(crate) enum FlycheckConfig {
94 CargoCommand {
95 command: String,
96 options: CargoOptions,
97 ansi_color_output: bool,
98 },
99 CustomCommand {
100 command: String,
101 args: Vec<String>,
102 extra_env: FxHashMap<String, Option<String>>,
103 invocation_strategy: InvocationStrategy,
104 },
105}
106
107impl FlycheckConfig {
108 pub(crate) fn invocation_strategy(&self) -> InvocationStrategy {
109 match self {
110 FlycheckConfig::CargoCommand { .. } => InvocationStrategy::PerWorkspace,
111 FlycheckConfig::CustomCommand { invocation_strategy, .. } => {
112 invocation_strategy.clone()
113 }
114 }
115 }
116}
117
118impl fmt::Display for FlycheckConfig {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
122 FlycheckConfig::CustomCommand { command, args, .. } => {
123 let display_args = args
130 .iter()
131 .map(|arg| if arg == SAVED_FILE_PLACEHOLDER { "..." } else { arg })
132 .collect::<Vec<_>>();
133
134 write!(f, "{command} {}", display_args.join(" "))
135 }
136 }
137 }
138}
139
140#[derive(Debug)]
145pub(crate) struct FlycheckHandle {
146 sender: Sender<StateChange>,
148 _thread: stdx::thread::JoinHandle,
149 id: usize,
150 generation: AtomicUsize,
151}
152
153impl FlycheckHandle {
154 pub(crate) fn spawn(
155 id: usize,
156 generation: DiagnosticsGeneration,
157 sender: Sender<FlycheckMessage>,
158 config: FlycheckConfig,
159 sysroot_root: Option<AbsPathBuf>,
160 workspace_root: AbsPathBuf,
161 manifest_path: Option<AbsPathBuf>,
162 ws_target_dir: Option<Utf8PathBuf>,
163 ) -> FlycheckHandle {
164 let actor = FlycheckActor::new(
165 id,
166 generation,
167 sender,
168 config,
169 sysroot_root,
170 workspace_root,
171 manifest_path,
172 ws_target_dir,
173 );
174 let (sender, receiver) = unbounded::<StateChange>();
175 let thread =
176 stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker, format!("Flycheck{id}"))
177 .spawn(move || actor.run(receiver))
178 .expect("failed to spawn thread");
179 FlycheckHandle { id, generation: generation.into(), sender, _thread: thread }
180 }
181
182 pub(crate) fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
184 let generation = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
185 self.sender
186 .send(StateChange::Restart {
187 generation,
188 scope: FlycheckScope::Workspace,
189 saved_file,
190 target: None,
191 })
192 .unwrap();
193 }
194
195 pub(crate) fn restart_for_package(
197 &self,
198 package: Arc<PackageId>,
199 target: Option<Target>,
200 workspace_deps: Option<FxHashSet<Arc<PackageId>>>,
201 ) {
202 let generation = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
203 self.sender
204 .send(StateChange::Restart {
205 generation,
206 scope: FlycheckScope::Package { package, workspace_deps },
207 saved_file: None,
208 target,
209 })
210 .unwrap();
211 }
212
213 pub(crate) fn cancel(&self) {
215 self.sender.send(StateChange::Cancel).unwrap();
216 }
217
218 pub(crate) fn id(&self) -> usize {
219 self.id
220 }
221
222 pub(crate) fn generation(&self) -> DiagnosticsGeneration {
223 self.generation.load(Ordering::Relaxed)
224 }
225}
226
227#[derive(Debug)]
228pub(crate) enum ClearDiagnosticsKind {
229 All(ClearScope),
230 OlderThan(DiagnosticsGeneration, ClearScope),
231}
232
233#[derive(Debug)]
234pub(crate) enum ClearScope {
235 Workspace,
236 Package(Arc<PackageId>),
237}
238
239pub(crate) enum FlycheckMessage {
240 AddDiagnostic {
242 id: usize,
243 generation: DiagnosticsGeneration,
244 workspace_root: Arc<AbsPathBuf>,
245 diagnostic: Diagnostic,
246 package_id: Option<Arc<PackageId>>,
247 },
248
249 ClearDiagnostics { id: usize, kind: ClearDiagnosticsKind },
251
252 Progress {
254 id: usize,
256 progress: Progress,
257 },
258}
259
260impl fmt::Debug for FlycheckMessage {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 match self {
263 FlycheckMessage::AddDiagnostic {
264 id,
265 generation,
266 workspace_root,
267 diagnostic,
268 package_id,
269 } => f
270 .debug_struct("AddDiagnostic")
271 .field("id", id)
272 .field("generation", generation)
273 .field("workspace_root", workspace_root)
274 .field("package_id", package_id)
275 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
276 .finish(),
277 FlycheckMessage::ClearDiagnostics { id, kind } => {
278 f.debug_struct("ClearDiagnostics").field("id", id).field("kind", kind).finish()
279 }
280 FlycheckMessage::Progress { id, progress } => {
281 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
282 }
283 }
284 }
285}
286
287#[derive(Debug)]
288pub(crate) enum Progress {
289 DidStart,
290 DidCheckCrate(String),
291 DidFinish(io::Result<()>),
292 DidCancel,
293 DidFailToRestart(String),
294}
295
296enum FlycheckScope {
297 Workspace,
298 Package { package: Arc<PackageId>, workspace_deps: Option<FxHashSet<Arc<PackageId>>> },
299}
300
301enum StateChange {
302 Restart {
303 generation: DiagnosticsGeneration,
304 scope: FlycheckScope,
305 saved_file: Option<AbsPathBuf>,
306 target: Option<Target>,
307 },
308 Cancel,
309}
310
311struct FlycheckActor {
313 id: usize,
315
316 generation: DiagnosticsGeneration,
317 sender: Sender<FlycheckMessage>,
318 config: FlycheckConfig,
319 manifest_path: Option<AbsPathBuf>,
320 ws_target_dir: Option<Utf8PathBuf>,
321 root: Arc<AbsPathBuf>,
324 sysroot_root: Option<AbsPathBuf>,
325 scope: FlycheckScope,
326 command_handle: Option<CommandHandle<CargoCheckMessage>>,
332 command_receiver: Option<Receiver<CargoCheckMessage>>,
334 diagnostics_cleared_for: FxHashSet<Arc<PackageId>>,
335 diagnostics_received: DiagnosticsReceived,
336}
337
338#[derive(PartialEq)]
339enum DiagnosticsReceived {
340 Yes,
341 No,
342 YesAndClearedForAll,
343}
344
345#[allow(clippy::large_enum_variant)]
346enum Event {
347 RequestStateChange(StateChange),
348 CheckEvent(Option<CargoCheckMessage>),
349}
350
351pub(crate) const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
352
353impl FlycheckActor {
354 fn new(
355 id: usize,
356 generation: DiagnosticsGeneration,
357 sender: Sender<FlycheckMessage>,
358 config: FlycheckConfig,
359 sysroot_root: Option<AbsPathBuf>,
360 workspace_root: AbsPathBuf,
361 manifest_path: Option<AbsPathBuf>,
362 ws_target_dir: Option<Utf8PathBuf>,
363 ) -> FlycheckActor {
364 tracing::info!(%id, ?workspace_root, "Spawning flycheck");
365 FlycheckActor {
366 id,
367 generation,
368 sender,
369 config,
370 sysroot_root,
371 root: Arc::new(workspace_root),
372 scope: FlycheckScope::Workspace,
373 manifest_path,
374 ws_target_dir,
375 command_handle: None,
376 command_receiver: None,
377 diagnostics_cleared_for: Default::default(),
378 diagnostics_received: DiagnosticsReceived::No,
379 }
380 }
381
382 fn report_progress(&self, progress: Progress) {
383 self.send(FlycheckMessage::Progress { id: self.id, progress });
384 }
385
386 fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
387 let Some(command_receiver) = &self.command_receiver else {
388 return inbox.recv().ok().map(Event::RequestStateChange);
389 };
390
391 select_biased! {
393 recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
394 recv(command_receiver) -> msg => Some(Event::CheckEvent(msg.ok())),
395 }
396 }
397
398 fn run(mut self, inbox: Receiver<StateChange>) {
399 'event: while let Some(event) = self.next_event(&inbox) {
400 match event {
401 Event::RequestStateChange(StateChange::Cancel) => {
402 tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
403 self.cancel_check_process();
404 }
405 Event::RequestStateChange(StateChange::Restart {
406 generation,
407 scope,
408 saved_file,
409 target,
410 }) => {
411 self.cancel_check_process();
413 while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
414 if let StateChange::Cancel = restart {
416 continue 'event;
417 }
418 }
419
420 let command = self.check_command(&scope, saved_file.as_deref(), target);
421 self.scope = scope;
422 self.generation = generation;
423
424 let Some(command) = command else {
425 continue;
426 };
427
428 let formatted_command = format!("{command:?}");
429
430 tracing::debug!(?command, "will restart flycheck");
431 let (sender, receiver) = unbounded();
432 match CommandHandle::spawn(
433 command,
434 CargoCheckParser,
435 sender,
436 match &self.config {
437 FlycheckConfig::CargoCommand { options, .. } => {
438 let ws_target_dir =
439 self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path);
440 let target_dir =
441 options.target_dir_config.target_dir(ws_target_dir);
442
443 let target_dir = target_dir.as_deref().or(ws_target_dir);
446
447 Some(
448 self.root
456 .join(target_dir.unwrap_or(
457 Utf8Path::new("target").join("rust-analyzer").as_path(),
458 ))
459 .join(format!("flycheck{}", self.id))
460 .into(),
461 )
462 }
463 _ => None,
464 },
465 ) {
466 Ok(command_handle) => {
467 tracing::debug!(command = formatted_command, "did restart flycheck");
468 self.command_handle = Some(command_handle);
469 self.command_receiver = Some(receiver);
470 self.report_progress(Progress::DidStart);
471 }
472 Err(error) => {
473 self.report_progress(Progress::DidFailToRestart(format!(
474 "Failed to run the following command: {formatted_command} error={error}"
475 )));
476 }
477 }
478 }
479 Event::CheckEvent(None) => {
480 tracing::debug!(flycheck_id = self.id, "flycheck finished");
481
482 let command_handle = self.command_handle.take().unwrap();
484 self.command_receiver.take();
485 let formatted_handle = format!("{command_handle:?}");
486
487 let res = command_handle.join();
488 if let Err(error) = &res {
489 tracing::error!(
490 "Flycheck failed to run the following command: {}, error={}",
491 formatted_handle,
492 error
493 );
494 }
495 if self.diagnostics_received == DiagnosticsReceived::No {
496 tracing::trace!(flycheck_id = self.id, "clearing diagnostics");
497 match &self.scope {
500 FlycheckScope::Workspace => {
501 self.send(FlycheckMessage::ClearDiagnostics {
502 id: self.id,
503 kind: ClearDiagnosticsKind::All(ClearScope::Workspace),
504 });
505 }
506 FlycheckScope::Package { package, workspace_deps } => {
507 for pkg in
508 std::iter::once(package).chain(workspace_deps.iter().flatten())
509 {
510 self.send(FlycheckMessage::ClearDiagnostics {
511 id: self.id,
512 kind: ClearDiagnosticsKind::All(ClearScope::Package(
513 pkg.clone(),
514 )),
515 });
516 }
517 }
518 }
519 } else if res.is_ok() {
520 match &self.scope {
525 FlycheckScope::Workspace => {
526 self.send(FlycheckMessage::ClearDiagnostics {
527 id: self.id,
528 kind: ClearDiagnosticsKind::OlderThan(
529 self.generation,
530 ClearScope::Workspace,
531 ),
532 });
533 }
534 FlycheckScope::Package { package, workspace_deps } => {
535 for pkg in
536 std::iter::once(package).chain(workspace_deps.iter().flatten())
537 {
538 self.send(FlycheckMessage::ClearDiagnostics {
539 id: self.id,
540 kind: ClearDiagnosticsKind::OlderThan(
541 self.generation,
542 ClearScope::Package(pkg.clone()),
543 ),
544 });
545 }
546 }
547 }
548 }
549 self.clear_diagnostics_state();
550
551 self.report_progress(Progress::DidFinish(res));
552 }
553 Event::CheckEvent(Some(message)) => match message {
554 CargoCheckMessage::CompilerArtifact(msg) => {
555 tracing::trace!(
556 flycheck_id = self.id,
557 artifact = msg.target.name,
558 package_id = msg.package_id.repr,
559 "artifact received"
560 );
561 self.report_progress(Progress::DidCheckCrate(format!(
562 "{} ({})",
563 msg.target.name,
564 msg.target.kind.iter().format_with(", ", |kind, f| f(&kind)),
565 )));
566 let package_id = Arc::new(msg.package_id);
567 if self.diagnostics_cleared_for.insert(package_id.clone()) {
568 tracing::trace!(
569 flycheck_id = self.id,
570 package_id = package_id.repr,
571 "clearing diagnostics"
572 );
573 self.send(FlycheckMessage::ClearDiagnostics {
574 id: self.id,
575 kind: ClearDiagnosticsKind::All(ClearScope::Package(package_id)),
576 });
577 }
578 }
579 CargoCheckMessage::Diagnostic { diagnostic, package_id } => {
580 tracing::trace!(
581 flycheck_id = self.id,
582 message = diagnostic.message,
583 package_id = package_id.as_ref().map(|it| &it.repr),
584 "diagnostic received"
585 );
586 if self.diagnostics_received == DiagnosticsReceived::No {
587 self.diagnostics_received = DiagnosticsReceived::Yes;
588 }
589 if let Some(package_id) = &package_id {
590 if self.diagnostics_cleared_for.insert(package_id.clone()) {
591 tracing::trace!(
592 flycheck_id = self.id,
593 package_id = package_id.repr,
594 "clearing diagnostics"
595 );
596 self.send(FlycheckMessage::ClearDiagnostics {
597 id: self.id,
598 kind: ClearDiagnosticsKind::All(ClearScope::Package(
599 package_id.clone(),
600 )),
601 });
602 }
603 } else if self.diagnostics_received
604 != DiagnosticsReceived::YesAndClearedForAll
605 {
606 self.diagnostics_received = DiagnosticsReceived::YesAndClearedForAll;
607 self.send(FlycheckMessage::ClearDiagnostics {
608 id: self.id,
609 kind: ClearDiagnosticsKind::All(ClearScope::Workspace),
610 });
611 }
612 self.send(FlycheckMessage::AddDiagnostic {
613 id: self.id,
614 generation: self.generation,
615 package_id,
616 workspace_root: self.root.clone(),
617 diagnostic,
618 });
619 }
620 },
621 }
622 }
623 self.cancel_check_process();
625 }
626
627 fn cancel_check_process(&mut self) {
628 if let Some(command_handle) = self.command_handle.take() {
629 tracing::debug!(
630 command = ?command_handle,
631 "did cancel flycheck"
632 );
633 command_handle.cancel();
634 self.command_receiver.take();
635 self.report_progress(Progress::DidCancel);
636 }
637 self.clear_diagnostics_state();
638 }
639
640 fn clear_diagnostics_state(&mut self) {
641 self.diagnostics_cleared_for.clear();
642 self.diagnostics_received = DiagnosticsReceived::No;
643 }
644
645 fn check_command(
649 &self,
650 scope: &FlycheckScope,
651 saved_file: Option<&AbsPath>,
652 target: Option<Target>,
653 ) -> Option<Command> {
654 match &self.config {
655 FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
656 let mut cmd =
657 toolchain::command(Tool::Cargo.path(), &*self.root, &options.extra_env);
658 if let Some(sysroot_root) = &self.sysroot_root
659 && !options.extra_env.contains_key("RUSTUP_TOOLCHAIN")
660 && std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
661 {
662 cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
663 }
664 cmd.env("CARGO_LOG", "cargo::core::compiler::fingerprint=info");
665 cmd.arg(command);
666
667 match scope {
668 FlycheckScope::Workspace => cmd.arg("--workspace"),
669 FlycheckScope::Package { package, .. } => cmd.arg("-p").arg(&package.repr),
670 };
671
672 if let Some(tgt) = target {
673 match tgt {
674 Target::Bin(tgt) => cmd.arg("--bin").arg(tgt),
675 Target::Example(tgt) => cmd.arg("--example").arg(tgt),
676 Target::Test(tgt) => cmd.arg("--test").arg(tgt),
677 Target::Benchmark(tgt) => cmd.arg("--bench").arg(tgt),
678 };
679 }
680
681 cmd.arg(if *ansi_color_output {
682 "--message-format=json-diagnostic-rendered-ansi"
683 } else {
684 "--message-format=json"
685 });
686
687 if let Some(manifest_path) = &self.manifest_path {
688 cmd.arg("--manifest-path");
689 cmd.arg(manifest_path);
690 if manifest_path.extension() == Some("rs") {
691 cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
692 cmd.arg("-Zscript");
693 }
694 }
695
696 cmd.arg("--keep-going");
697
698 options.apply_on_command(
699 &mut cmd,
700 self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path),
701 );
702 cmd.args(&options.extra_args);
703 Some(cmd)
704 }
705 FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => {
706 let root = match invocation_strategy {
707 InvocationStrategy::Once => &*self.root,
708 InvocationStrategy::PerWorkspace => {
709 &*self.root
711 }
712 };
713 let mut cmd = toolchain::command(command, root, extra_env);
714
715 if let Some(saved_file) = saved_file {
718 for arg in args {
719 if arg == SAVED_FILE_PLACEHOLDER {
720 cmd.arg(saved_file);
721 } else {
722 cmd.arg(arg);
723 }
724 }
725 } else {
726 for arg in args {
727 if arg == SAVED_FILE_PLACEHOLDER {
728 return None;
731 }
732
733 cmd.arg(arg);
734 }
735 }
736
737 Some(cmd)
738 }
739 }
740 }
741
742 #[track_caller]
743 fn send(&self, check_task: FlycheckMessage) {
744 self.sender.send(check_task).unwrap();
745 }
746}
747
748#[allow(clippy::large_enum_variant)]
749enum CargoCheckMessage {
750 CompilerArtifact(cargo_metadata::Artifact),
751 Diagnostic { diagnostic: Diagnostic, package_id: Option<Arc<PackageId>> },
752}
753
754struct CargoCheckParser;
755
756impl JsonLinesParser<CargoCheckMessage> for CargoCheckParser {
757 fn from_line(&self, line: &str, error: &mut String) -> Option<CargoCheckMessage> {
758 let mut deserializer = serde_json::Deserializer::from_str(line);
759 deserializer.disable_recursion_limit();
760 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
761 return match message {
762 JsonMessage::Cargo(message) => match message {
764 cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
765 Some(CargoCheckMessage::CompilerArtifact(artifact))
766 }
767 cargo_metadata::Message::CompilerMessage(msg) => {
768 Some(CargoCheckMessage::Diagnostic {
769 diagnostic: msg.message,
770 package_id: Some(Arc::new(msg.package_id)),
771 })
772 }
773 _ => None,
774 },
775 JsonMessage::Rustc(message) => {
776 Some(CargoCheckMessage::Diagnostic { diagnostic: message, package_id: None })
777 }
778 };
779 }
780
781 error.push_str(line);
782 error.push('\n');
783 None
784 }
785
786 fn from_eof(&self) -> Option<CargoCheckMessage> {
787 None
788 }
789}
790
791#[derive(Deserialize)]
792#[serde(untagged)]
793enum JsonMessage {
794 Cargo(cargo_metadata::Message),
795 Rustc(Diagnostic),
796}