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 project_model::project_json;
18use rustc_hash::FxHashMap;
19use serde::Deserialize as _;
20use serde_derive::Deserialize;
21
22pub(crate) use cargo_metadata::diagnostic::{
23 Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
24};
25use toolchain::Tool;
26use triomphe::Arc;
27
28use crate::{
29 command::{CommandHandle, JsonLinesParser},
30 diagnostics::DiagnosticsGeneration,
31};
32
33#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub(crate) enum InvocationStrategy {
35 Once,
36 #[default]
37 PerWorkspace,
38}
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub(crate) struct CargoOptions {
43 pub(crate) subcommand: String,
45 pub(crate) target_tuples: Vec<String>,
46 pub(crate) all_targets: bool,
47 pub(crate) set_test: bool,
48 pub(crate) no_default_features: bool,
49 pub(crate) all_features: bool,
50 pub(crate) features: Vec<String>,
51 pub(crate) extra_args: Vec<String>,
52 pub(crate) extra_test_bin_args: Vec<String>,
53 pub(crate) extra_env: FxHashMap<String, Option<String>>,
54 pub(crate) target_dir_config: TargetDirectoryConfig,
55}
56
57#[derive(Clone, Debug)]
58pub(crate) enum Target {
59 Bin(String),
60 Example(String),
61 Benchmark(String),
62 Test(String),
63}
64
65impl CargoOptions {
66 pub(crate) fn apply_on_command(&self, cmd: &mut Command, ws_target_dir: Option<&Utf8Path>) {
67 for target in &self.target_tuples {
68 cmd.args(["--target", target.as_str()]);
69 }
70 if self.all_targets {
71 if self.set_test {
72 cmd.arg("--all-targets");
73 } else {
74 cmd.args(["--lib", "--bins", "--examples"]);
77 }
78 }
79 if self.all_features {
80 cmd.arg("--all-features");
81 } else {
82 if self.no_default_features {
83 cmd.arg("--no-default-features");
84 }
85 if !self.features.is_empty() {
86 cmd.arg("--features");
87 cmd.arg(self.features.join(" "));
88 }
89 }
90 if let Some(target_dir) = self.target_dir_config.target_dir(ws_target_dir) {
91 cmd.arg("--target-dir").arg(target_dir.as_ref());
92 }
93 }
94}
95
96#[derive(Debug, Default)]
98pub(crate) struct FlycheckConfigJson {
99 pub single_template: Option<project_json::Runnable>,
101}
102
103impl FlycheckConfigJson {
104 pub(crate) fn any_configured(&self) -> bool {
105 self.single_template.is_some()
107 }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
115pub(crate) enum FlycheckConfig {
116 Automatic {
121 cargo_options: CargoOptions,
123 ansi_color_output: bool,
124 },
125 CustomCommand {
127 command: String,
128 args: Vec<String>,
129 extra_env: FxHashMap<String, Option<String>>,
130 invocation_strategy: InvocationStrategy,
131 },
132}
133
134impl FlycheckConfig {
135 pub(crate) fn invocation_strategy(&self) -> InvocationStrategy {
136 match self {
137 FlycheckConfig::Automatic { .. } => InvocationStrategy::PerWorkspace,
138 FlycheckConfig::CustomCommand { invocation_strategy, .. } => {
139 invocation_strategy.clone()
140 }
141 }
142 }
143}
144
145impl fmt::Display for FlycheckConfig {
146 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 match self {
149 FlycheckConfig::Automatic { cargo_options, .. } => {
150 write!(f, "cargo {}", cargo_options.subcommand)
151 }
152 FlycheckConfig::CustomCommand { command, args, .. } => {
153 let display_args = args
162 .iter()
163 .map(|arg| {
164 if (arg == SAVED_FILE_PLACEHOLDER_DOLLAR)
165 || (arg == SAVED_FILE_INLINE)
166 || arg.ends_with(".rs")
167 {
168 "..."
169 } else {
170 arg
171 }
172 })
173 .collect::<Vec<_>>();
174
175 write!(f, "{command} {}", display_args.join(" "))
176 }
177 }
178 }
179}
180
181#[derive(Debug)]
186pub(crate) struct FlycheckHandle {
187 sender: Sender<StateChange>,
189 _thread: stdx::thread::JoinHandle,
190 id: usize,
191 generation: Arc<AtomicUsize>,
192}
193
194impl FlycheckHandle {
195 pub(crate) fn spawn(
196 id: usize,
197 generation: Arc<AtomicUsize>,
198 sender: Sender<FlycheckMessage>,
199 config: FlycheckConfig,
200 config_json: FlycheckConfigJson,
201 sysroot_root: Option<AbsPathBuf>,
202 workspace_root: AbsPathBuf,
203 manifest_path: Option<AbsPathBuf>,
204 ws_target_dir: Option<Utf8PathBuf>,
205 ) -> FlycheckHandle {
206 let actor = FlycheckActor::new(
207 id,
208 generation.load(Ordering::Relaxed),
209 sender,
210 config,
211 config_json,
212 sysroot_root,
213 workspace_root,
214 manifest_path,
215 ws_target_dir,
216 );
217 let (sender, receiver) = unbounded::<StateChange>();
218 let thread =
219 stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker, format!("Flycheck{id}"))
220 .spawn(move || actor.run(receiver))
221 .expect("failed to spawn thread");
222 FlycheckHandle { id, generation, sender, _thread: thread }
223 }
224
225 pub(crate) fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
227 let generation = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
228 self.sender
229 .send(StateChange::Restart {
230 generation,
231 scope: FlycheckScope::Workspace,
232 saved_file,
233 target: None,
234 })
235 .unwrap();
236 }
237
238 pub(crate) fn restart_for_package(
240 &self,
241 package: PackageSpecifier,
242 target: Option<Target>,
243 workspace_deps: Option<FxHashSet<PackageSpecifier>>,
244 saved_file: Option<AbsPathBuf>,
245 ) {
246 let generation = self.generation.fetch_add(1, Ordering::Relaxed) + 1;
247 self.sender
248 .send(StateChange::Restart {
249 generation,
250 scope: FlycheckScope::Package { package, workspace_deps },
251 saved_file,
252 target,
253 })
254 .unwrap();
255 }
256
257 pub(crate) fn cancel(&self) {
259 self.sender.send(StateChange::Cancel).unwrap();
260 }
261
262 pub(crate) fn id(&self) -> usize {
263 self.id
264 }
265
266 pub(crate) fn generation(&self) -> DiagnosticsGeneration {
267 self.generation.load(Ordering::Relaxed)
268 }
269}
270
271#[derive(Debug)]
272pub(crate) enum ClearDiagnosticsKind {
273 All(ClearScope),
274 OlderThan(DiagnosticsGeneration, ClearScope),
275}
276
277#[derive(Debug)]
278pub(crate) enum ClearScope {
279 Workspace,
280 Package(PackageSpecifier),
281}
282
283pub(crate) enum FlycheckMessage {
284 AddDiagnostic {
286 id: usize,
287 generation: DiagnosticsGeneration,
288 workspace_root: Arc<AbsPathBuf>,
289 diagnostic: Diagnostic,
290 package_id: Option<PackageSpecifier>,
291 },
292
293 ClearDiagnostics { id: usize, kind: ClearDiagnosticsKind },
295
296 Progress {
298 id: usize,
300 progress: Progress,
301 },
302}
303
304impl fmt::Debug for FlycheckMessage {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
306 match self {
307 FlycheckMessage::AddDiagnostic {
308 id,
309 generation,
310 workspace_root,
311 diagnostic,
312 package_id,
313 } => f
314 .debug_struct("AddDiagnostic")
315 .field("id", id)
316 .field("generation", generation)
317 .field("workspace_root", workspace_root)
318 .field("package_id", package_id)
319 .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
320 .finish(),
321 FlycheckMessage::ClearDiagnostics { id, kind } => {
322 f.debug_struct("ClearDiagnostics").field("id", id).field("kind", kind).finish()
323 }
324 FlycheckMessage::Progress { id, progress } => {
325 f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
326 }
327 }
328 }
329}
330
331#[derive(Debug)]
332pub(crate) enum Progress {
333 DidStart {
334 user_facing_command: String,
337 },
338 DidCheckCrate(String),
339 DidFinish(io::Result<()>),
340 DidCancel,
341 DidFailToRestart(String),
342}
343
344#[derive(Debug, Clone)]
345enum FlycheckScope {
346 Workspace,
347 Package {
348 package: PackageSpecifier,
350 workspace_deps: Option<FxHashSet<PackageSpecifier>>,
351 },
352}
353
354#[derive(Debug, Hash, PartialEq, Eq, Clone)]
355pub(crate) enum PackageSpecifier {
356 Cargo {
357 package_id: Arc<PackageId>,
359 },
360 BuildInfo {
361 label: String,
363 },
364}
365
366impl PackageSpecifier {
367 pub(crate) fn as_str(&self) -> &str {
368 match self {
369 Self::Cargo { package_id } => &package_id.repr,
370 Self::BuildInfo { label } => label,
371 }
372 }
373}
374
375#[derive(Debug)]
376enum FlycheckCommandOrigin {
377 Cargo,
379 CheckOverrideCommand,
381 ProjectJsonRunnable,
383}
384
385#[derive(Debug)]
386enum StateChange {
387 Restart {
388 generation: DiagnosticsGeneration,
389 scope: FlycheckScope,
390 saved_file: Option<AbsPathBuf>,
391 target: Option<Target>,
392 },
393 Cancel,
394}
395
396struct FlycheckActor {
398 id: usize,
400
401 generation: DiagnosticsGeneration,
402 sender: Sender<FlycheckMessage>,
403 config: FlycheckConfig,
404 config_json: FlycheckConfigJson,
405
406 manifest_path: Option<AbsPathBuf>,
407 ws_target_dir: Option<Utf8PathBuf>,
408 root: Arc<AbsPathBuf>,
411 sysroot_root: Option<AbsPathBuf>,
412 scope: FlycheckScope,
413 command_handle: Option<CommandHandle<CheckMessage>>,
419 command_receiver: Option<Receiver<CheckMessage>>,
421 diagnostics_cleared_for: FxHashSet<PackageSpecifier>,
422 diagnostics_received: DiagnosticsReceived,
423}
424
425#[derive(PartialEq, Debug)]
426enum DiagnosticsReceived {
427 NotYet,
429 AtLeastOne,
433 AtLeastOneAndClearedWorkspace,
436}
437
438#[allow(clippy::large_enum_variant)]
439#[derive(Debug)]
440enum Event {
441 RequestStateChange(StateChange),
442 CheckEvent(Option<CheckMessage>),
443}
444
445const SAVED_FILE_PLACEHOLDER_DOLLAR: &str = "$saved_file";
447const LABEL_INLINE: &str = "{label}";
448const SAVED_FILE_INLINE: &str = "{saved_file}";
449
450#[derive(Debug)]
451struct Substitutions<'a> {
452 label: Option<&'a str>,
453 saved_file: Option<&'a str>,
454}
455
456impl<'a> Substitutions<'a> {
457 #[allow(clippy::disallowed_types)] fn substitute<H>(
465 self,
466 template: &project_json::Runnable,
467 extra_env: &std::collections::HashMap<String, Option<String>, H>,
468 ) -> Option<Command> {
469 let mut cmd = toolchain::command(&template.program, &template.cwd, extra_env);
470 for arg in &template.args {
471 if let Some(ix) = arg.find(LABEL_INLINE) {
472 if let Some(label) = self.label {
473 let mut arg = arg.to_string();
474 arg.replace_range(ix..ix + LABEL_INLINE.len(), label);
475 cmd.arg(arg);
476 continue;
477 } else {
478 return None;
479 }
480 }
481 if let Some(ix) = arg.find(SAVED_FILE_INLINE) {
482 if let Some(saved_file) = self.saved_file {
483 let mut arg = arg.to_string();
484 arg.replace_range(ix..ix + SAVED_FILE_INLINE.len(), saved_file);
485 cmd.arg(arg);
486 continue;
487 } else {
488 return None;
489 }
490 }
491 if arg == SAVED_FILE_PLACEHOLDER_DOLLAR {
493 if let Some(saved_file) = self.saved_file {
494 cmd.arg(saved_file);
495 continue;
496 } else {
497 return None;
498 }
499 }
500 cmd.arg(arg);
501 }
502 cmd.current_dir(&template.cwd);
503 Some(cmd)
504 }
505}
506
507impl FlycheckActor {
508 fn new(
509 id: usize,
510 generation: DiagnosticsGeneration,
511 sender: Sender<FlycheckMessage>,
512 config: FlycheckConfig,
513 config_json: FlycheckConfigJson,
514 sysroot_root: Option<AbsPathBuf>,
515 workspace_root: AbsPathBuf,
516 manifest_path: Option<AbsPathBuf>,
517 ws_target_dir: Option<Utf8PathBuf>,
518 ) -> FlycheckActor {
519 tracing::info!(%id, ?workspace_root, "Spawning flycheck");
520 FlycheckActor {
521 id,
522 generation,
523 sender,
524 config,
525 config_json,
526 sysroot_root,
527 root: Arc::new(workspace_root),
528 scope: FlycheckScope::Workspace,
529 manifest_path,
530 ws_target_dir,
531 command_handle: None,
532 command_receiver: None,
533 diagnostics_cleared_for: Default::default(),
534 diagnostics_received: DiagnosticsReceived::NotYet,
535 }
536 }
537
538 fn report_progress(&self, progress: Progress) {
539 self.send(FlycheckMessage::Progress { id: self.id, progress });
540 }
541
542 fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
543 let Some(command_receiver) = &self.command_receiver else {
544 return inbox.recv().ok().map(Event::RequestStateChange);
545 };
546
547 select_biased! {
549 recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
550 recv(command_receiver) -> msg => Some(Event::CheckEvent(msg.ok())),
551 }
552 }
553
554 fn run(mut self, inbox: Receiver<StateChange>) {
555 'event: while let Some(event) = self.next_event(&inbox) {
556 match event {
557 Event::RequestStateChange(StateChange::Cancel) => {
558 tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
559 self.cancel_check_process();
560 }
561 Event::RequestStateChange(StateChange::Restart {
562 mut generation,
563 mut scope,
564 mut saved_file,
565 mut target,
566 }) => {
567 self.cancel_check_process();
569
570 while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
572 match restart {
573 StateChange::Cancel => {
574 continue 'event;
577 }
578 StateChange::Restart {
579 generation: g,
580 scope: s,
581 saved_file: sf,
582 target: t,
583 } => {
584 generation = g;
589 scope = s;
590 saved_file = sf;
591 target = t;
592 }
593 }
594 }
595
596 let command = self.check_command(&scope, saved_file.as_deref(), target);
597 self.scope = scope.clone();
598 self.generation = generation;
599
600 let Some((command, origin)) = command else {
601 tracing::debug!(?scope, "failed to build flycheck command");
602 continue;
603 };
604
605 let debug_command = format!("{command:?}");
606 let user_facing_command = self.config.to_string();
607
608 tracing::debug!(?origin, ?command, "will restart flycheck");
609 let (sender, receiver) = unbounded();
610 match CommandHandle::spawn(
611 command,
612 CheckParser,
613 sender,
614 match &self.config {
615 FlycheckConfig::Automatic { cargo_options, .. } => {
616 let ws_target_dir =
617 self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path);
618 let target_dir =
619 cargo_options.target_dir_config.target_dir(ws_target_dir);
620
621 let target_dir = target_dir.as_deref().or(ws_target_dir);
624
625 Some(
626 self.root
634 .join(target_dir.unwrap_or(
635 Utf8Path::new("target").join("rust-analyzer").as_path(),
636 ))
637 .join(format!("flycheck{}", self.id))
638 .into(),
639 )
640 }
641 _ => None,
642 },
643 ) {
644 Ok(command_handle) => {
645 tracing::debug!(?origin, command = %debug_command, "did restart flycheck");
646 self.command_handle = Some(command_handle);
647 self.command_receiver = Some(receiver);
648 self.report_progress(Progress::DidStart { user_facing_command });
649 }
650 Err(error) => {
651 self.report_progress(Progress::DidFailToRestart(format!(
652 "Failed to run the following command: {debug_command} origin={origin:?} error={error}"
653 )));
654 }
655 }
656 }
657 Event::CheckEvent(None) => {
658 tracing::debug!(flycheck_id = self.id, "flycheck finished");
659
660 let command_handle = self.command_handle.take().unwrap();
662 self.command_receiver.take();
663 let formatted_handle = format!("{command_handle:?}");
664
665 let res = command_handle.join();
666 if let Err(error) = &res {
667 tracing::error!(
668 "Flycheck failed to run the following command: {}, error={}",
669 formatted_handle,
670 error
671 );
672 }
673 if self.diagnostics_received == DiagnosticsReceived::NotYet {
674 tracing::trace!(flycheck_id = self.id, "clearing diagnostics");
675 self.send(FlycheckMessage::ClearDiagnostics {
698 id: self.id,
699 kind: ClearDiagnosticsKind::All(ClearScope::Workspace),
700 });
701 } else if res.is_ok() {
702 match &self.scope {
707 FlycheckScope::Workspace => {
708 self.send(FlycheckMessage::ClearDiagnostics {
709 id: self.id,
710 kind: ClearDiagnosticsKind::OlderThan(
711 self.generation,
712 ClearScope::Workspace,
713 ),
714 });
715 }
716 FlycheckScope::Package { package, workspace_deps } => {
717 for pkg in
718 std::iter::once(package).chain(workspace_deps.iter().flatten())
719 {
720 self.send(FlycheckMessage::ClearDiagnostics {
721 id: self.id,
722 kind: ClearDiagnosticsKind::OlderThan(
723 self.generation,
724 ClearScope::Package(pkg.clone()),
725 ),
726 });
727 }
728 }
729 }
730 }
731 self.clear_diagnostics_state();
732
733 self.report_progress(Progress::DidFinish(res));
734 }
735 Event::CheckEvent(Some(message)) => match message {
736 CheckMessage::CompilerArtifact(msg) => {
737 tracing::trace!(
738 flycheck_id = self.id,
739 artifact = msg.target.name,
740 package_id = msg.package_id.repr,
741 "artifact received"
742 );
743 self.report_progress(Progress::DidCheckCrate(format!(
744 "{} ({})",
745 msg.target.name,
746 msg.target.kind.iter().format_with(", ", |kind, f| f(&kind)),
747 )));
748 let package_id = Arc::new(msg.package_id);
749 if self
750 .diagnostics_cleared_for
751 .insert(PackageSpecifier::Cargo { package_id: package_id.clone() })
752 {
753 tracing::trace!(
754 flycheck_id = self.id,
755 package_id = package_id.repr,
756 "clearing diagnostics"
757 );
758 self.send(FlycheckMessage::ClearDiagnostics {
759 id: self.id,
760 kind: ClearDiagnosticsKind::All(ClearScope::Package(
761 PackageSpecifier::Cargo { package_id },
762 )),
763 });
764 }
765 }
766 CheckMessage::Diagnostic { diagnostic, package_id } => {
767 tracing::trace!(
768 flycheck_id = self.id,
769 message = diagnostic.message,
770 package_id = package_id.as_ref().map(|it| it.as_str()),
771 "diagnostic received"
772 );
773 if self.diagnostics_received == DiagnosticsReceived::NotYet {
774 self.diagnostics_received = DiagnosticsReceived::AtLeastOne;
775 }
776 if let Some(package_id) = &package_id {
777 if self.diagnostics_cleared_for.insert(package_id.clone()) {
778 tracing::trace!(
779 flycheck_id = self.id,
780 package_id = package_id.as_str(),
781 "clearing diagnostics"
782 );
783 self.send(FlycheckMessage::ClearDiagnostics {
784 id: self.id,
785 kind: ClearDiagnosticsKind::All(ClearScope::Package(
786 package_id.clone(),
787 )),
788 });
789 }
790 } else if self.diagnostics_received
791 != DiagnosticsReceived::AtLeastOneAndClearedWorkspace
792 {
793 self.diagnostics_received =
794 DiagnosticsReceived::AtLeastOneAndClearedWorkspace;
795 self.send(FlycheckMessage::ClearDiagnostics {
796 id: self.id,
797 kind: ClearDiagnosticsKind::All(ClearScope::Workspace),
798 });
799 }
800 self.send(FlycheckMessage::AddDiagnostic {
801 id: self.id,
802 generation: self.generation,
803 package_id,
804 workspace_root: self.root.clone(),
805 diagnostic,
806 });
807 }
808 },
809 }
810 }
811 self.cancel_check_process();
813 }
814
815 fn cancel_check_process(&mut self) {
816 if let Some(command_handle) = self.command_handle.take() {
817 tracing::debug!(
818 command = ?command_handle,
819 "did cancel flycheck"
820 );
821 command_handle.cancel();
822 self.command_receiver.take();
823 self.report_progress(Progress::DidCancel);
824 }
825 self.clear_diagnostics_state();
826 }
827
828 fn clear_diagnostics_state(&mut self) {
829 self.diagnostics_cleared_for.clear();
830 self.diagnostics_received = DiagnosticsReceived::NotYet;
831 }
832
833 fn explicit_check_command(
834 &self,
835 scope: &FlycheckScope,
836 saved_file: Option<&AbsPath>,
837 ) -> Option<Command> {
838 let label = match scope {
839 FlycheckScope::Workspace => return None,
843 FlycheckScope::Package { package: PackageSpecifier::BuildInfo { label }, .. } => {
844 label.as_str()
845 }
846 FlycheckScope::Package {
847 package: PackageSpecifier::Cargo { package_id: label },
848 ..
849 } => &label.repr,
850 };
851 let template = self.config_json.single_template.as_ref()?;
852 let subs = Substitutions { label: Some(label), saved_file: saved_file.map(|x| x.as_str()) };
853 subs.substitute(template, &FxHashMap::default())
854 }
855
856 fn check_command(
860 &self,
861 scope: &FlycheckScope,
862 saved_file: Option<&AbsPath>,
863 target: Option<Target>,
864 ) -> Option<(Command, FlycheckCommandOrigin)> {
865 match &self.config {
866 FlycheckConfig::Automatic { cargo_options, ansi_color_output } => {
867 if self.config_json.any_configured() {
875 let cmd = self.explicit_check_command(scope, saved_file)?;
879 return Some((cmd, FlycheckCommandOrigin::ProjectJsonRunnable));
880 }
881
882 let mut cmd =
883 toolchain::command(Tool::Cargo.path(), &*self.root, &cargo_options.extra_env);
884 if let Some(sysroot_root) = &self.sysroot_root
885 && !cargo_options.extra_env.contains_key("RUSTUP_TOOLCHAIN")
886 && std::env::var_os("RUSTUP_TOOLCHAIN").is_none()
887 {
888 cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
889 }
890 cmd.env("CARGO_LOG", "cargo::core::compiler::fingerprint=info");
891 cmd.arg(&cargo_options.subcommand);
892
893 match scope {
894 FlycheckScope::Workspace => cmd.arg("--workspace"),
895 FlycheckScope::Package {
896 package: PackageSpecifier::Cargo { package_id },
897 ..
898 } => cmd.arg("-p").arg(&package_id.repr),
899 FlycheckScope::Package {
900 package: PackageSpecifier::BuildInfo { .. }, ..
901 } => {
902 return None;
906 }
907 };
908
909 if let Some(tgt) = target {
910 match tgt {
911 Target::Bin(tgt) => cmd.arg("--bin").arg(tgt),
912 Target::Example(tgt) => cmd.arg("--example").arg(tgt),
913 Target::Test(tgt) => cmd.arg("--test").arg(tgt),
914 Target::Benchmark(tgt) => cmd.arg("--bench").arg(tgt),
915 };
916 }
917
918 cmd.arg(if *ansi_color_output {
919 "--message-format=json-diagnostic-rendered-ansi"
920 } else {
921 "--message-format=json"
922 });
923
924 if let Some(manifest_path) = &self.manifest_path {
925 cmd.arg("--manifest-path");
926 cmd.arg(manifest_path);
927 if manifest_path.extension() == Some("rs") {
928 cmd.env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly");
929 cmd.arg("-Zscript");
930 }
931 }
932
933 cmd.arg("--keep-going");
934
935 cargo_options.apply_on_command(
936 &mut cmd,
937 self.ws_target_dir.as_ref().map(Utf8PathBuf::as_path),
938 );
939 cmd.args(&cargo_options.extra_args);
940 Some((cmd, FlycheckCommandOrigin::Cargo))
941 }
942 FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => {
943 let root = match invocation_strategy {
944 InvocationStrategy::Once => &*self.root,
945 InvocationStrategy::PerWorkspace => {
946 &*self.root
948 }
949 };
950 let runnable = project_json::Runnable {
951 program: command.clone(),
952 cwd: Utf8Path::to_owned(root.as_ref()),
953 args: args.clone(),
954 kind: project_json::RunnableKind::Flycheck,
955 };
956
957 let label = match scope {
958 FlycheckScope::Workspace => None,
959 FlycheckScope::Package { package, .. } => Some(package.as_str()),
963 };
964
965 let subs = Substitutions { label, saved_file: saved_file.map(|x| x.as_str()) };
966 let cmd = subs.substitute(&runnable, extra_env)?;
967
968 Some((cmd, FlycheckCommandOrigin::CheckOverrideCommand))
969 }
970 }
971 }
972
973 #[track_caller]
974 fn send(&self, check_task: FlycheckMessage) {
975 self.sender.send(check_task).unwrap();
976 }
977}
978
979#[allow(clippy::large_enum_variant)]
980#[derive(Debug)]
981enum CheckMessage {
982 CompilerArtifact(cargo_metadata::Artifact),
985 Diagnostic { diagnostic: Diagnostic, package_id: Option<PackageSpecifier> },
987}
988
989struct CheckParser;
990
991impl JsonLinesParser<CheckMessage> for CheckParser {
992 fn from_line(&self, line: &str, error: &mut String) -> Option<CheckMessage> {
993 let mut deserializer = serde_json::Deserializer::from_str(line);
994 deserializer.disable_recursion_limit();
995 if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
996 return match message {
997 JsonMessage::Cargo(message) => match message {
999 cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
1000 Some(CheckMessage::CompilerArtifact(artifact))
1001 }
1002 cargo_metadata::Message::CompilerMessage(msg) => {
1003 Some(CheckMessage::Diagnostic {
1004 diagnostic: msg.message,
1005 package_id: Some(PackageSpecifier::Cargo {
1006 package_id: Arc::new(msg.package_id),
1007 }),
1008 })
1009 }
1010 _ => None,
1011 },
1012 JsonMessage::Rustc(message) => {
1013 Some(CheckMessage::Diagnostic { diagnostic: message, package_id: None })
1014 }
1015 };
1016 }
1017
1018 error.push_str(line);
1019 error.push('\n');
1020 None
1021 }
1022
1023 fn from_eof(&self) -> Option<CheckMessage> {
1024 None
1025 }
1026}
1027
1028#[derive(Deserialize, Debug)]
1029#[serde(untagged)]
1030enum JsonMessage {
1031 Cargo(cargo_metadata::Message),
1032 Rustc(Diagnostic),
1033}
1034
1035#[cfg(test)]
1036mod tests {
1037 use super::*;
1038 use ide_db::FxHashMap;
1039 use itertools::Itertools;
1040 use paths::Utf8Path;
1041 use project_model::project_json;
1042
1043 #[test]
1044 fn test_substitutions() {
1045 let label = ":label";
1046 let saved_file = "file.rs";
1047
1048 assert_eq!(test_substitute(None, None, "{label} {saved_file}").as_deref(), None);
1050 assert_eq!(test_substitute(Some(label), None, "{label} {saved_file}").as_deref(), None);
1051 assert_eq!(
1052 test_substitute(None, Some(saved_file), "{label} {saved_file}").as_deref(),
1053 None
1054 );
1055 assert_eq!(
1056 test_substitute(Some(label), Some(saved_file), "{label} {saved_file}").as_deref(),
1057 Some("build :label file.rs")
1058 );
1059
1060 assert_eq!(test_substitute(None, None, "{label}").as_deref(), None);
1062 assert_eq!(test_substitute(Some(label), None, "{label}").as_deref(), Some("build :label"),);
1063 assert_eq!(test_substitute(None, Some(saved_file), "{label}").as_deref(), None,);
1064 assert_eq!(
1065 test_substitute(Some(label), Some(saved_file), "{label}").as_deref(),
1066 Some("build :label"),
1067 );
1068
1069 assert_eq!(test_substitute(None, None, "{saved_file}").as_deref(), None);
1071 assert_eq!(test_substitute(Some(label), None, "{saved_file}").as_deref(), None);
1072 assert_eq!(
1073 test_substitute(None, Some(saved_file), "{saved_file}").as_deref(),
1074 Some("build file.rs")
1075 );
1076 assert_eq!(
1077 test_substitute(Some(label), Some(saved_file), "{saved_file}").as_deref(),
1078 Some("build file.rs")
1079 );
1080
1081 assert_eq!(test_substitute(None, None, "xxx").as_deref(), Some("build xxx"));
1083 assert_eq!(test_substitute(Some(label), None, "xxx").as_deref(), Some("build xxx"));
1084 assert_eq!(test_substitute(None, Some(saved_file), "xxx").as_deref(), Some("build xxx"));
1085 assert_eq!(
1086 test_substitute(Some(label), Some(saved_file), "xxx").as_deref(),
1087 Some("build xxx")
1088 );
1089
1090 assert_eq!(
1092 test_substitute(Some(label), None, "--label={label}").as_deref(),
1093 Some("build --label=:label")
1094 );
1095
1096 assert_eq!(
1098 test_substitute(None, Some(saved_file), "--saved={saved_file}").as_deref(),
1099 Some("build --saved=file.rs")
1100 );
1101
1102 assert_eq!(
1104 test_substitute(None, Some(saved_file), "$saved_file").as_deref(),
1105 Some("build file.rs")
1106 );
1107
1108 fn test_substitute(
1109 label: Option<&str>,
1110 saved_file: Option<&str>,
1111 args: &str,
1112 ) -> Option<String> {
1113 Substitutions { label, saved_file }
1114 .substitute(
1115 &project_json::Runnable {
1116 program: "build".to_owned(),
1117 args: Vec::from_iter(args.split_whitespace().map(ToOwned::to_owned)),
1118 cwd: Utf8Path::new("/path").to_owned(),
1119 kind: project_json::RunnableKind::Flycheck,
1120 },
1121 &FxHashMap::default(),
1122 )
1123 .map(|command| {
1124 command.get_args().map(|x| x.to_string_lossy()).collect_vec().join(" ")
1125 })
1126 .map(|args| format!("build {}", args))
1127 }
1128 }
1129
1130 #[test]
1131 fn test_flycheck_config_display() {
1132 let clippy = FlycheckConfig::Automatic {
1133 cargo_options: CargoOptions {
1134 subcommand: "clippy".to_owned(),
1135 target_tuples: vec![],
1136 all_targets: false,
1137 set_test: false,
1138 no_default_features: false,
1139 all_features: false,
1140 features: vec![],
1141 extra_args: vec![],
1142 extra_test_bin_args: vec![],
1143 extra_env: FxHashMap::default(),
1144 target_dir_config: TargetDirectoryConfig::default(),
1145 },
1146 ansi_color_output: true,
1147 };
1148 assert_eq!(clippy.to_string(), "cargo clippy");
1149
1150 let custom_dollar = FlycheckConfig::CustomCommand {
1151 command: "check".to_owned(),
1152 args: vec!["--input".to_owned(), "$saved_file".to_owned()],
1153 extra_env: FxHashMap::default(),
1154 invocation_strategy: InvocationStrategy::Once,
1155 };
1156 assert_eq!(custom_dollar.to_string(), "check --input ...");
1157
1158 let custom_inline = FlycheckConfig::CustomCommand {
1159 command: "check".to_owned(),
1160 args: vec!["--input".to_owned(), "{saved_file}".to_owned()],
1161 extra_env: FxHashMap::default(),
1162 invocation_strategy: InvocationStrategy::Once,
1163 };
1164 assert_eq!(custom_inline.to_string(), "check --input ...");
1165
1166 let custom_rs = FlycheckConfig::CustomCommand {
1167 command: "check".to_owned(),
1168 args: vec!["--input".to_owned(), "/path/to/file.rs".to_owned()],
1169 extra_env: FxHashMap::default(),
1170 invocation_strategy: InvocationStrategy::Once,
1171 };
1172 assert_eq!(custom_rs.to_string(), "check --input ...");
1173 }
1174}