rust_analyzer/
flycheck.rs

1//! Flycheck provides the functionality needed to run `cargo check` to provide
2//! LSP diagnostics based on the output of the command.
3
4use 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                // No --benches unfortunately, as this implies --tests (see https://github.com/rust-lang/cargo/issues/6454),
71                // and users setting `cfg.seTest = false` probably prefer disabling benches than enabling tests.
72                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                // Don't show `my_custom_check --foo $saved_file` literally to the user, as it
124                // looks like we've forgotten to substitute $saved_file.
125                //
126                // Instead, show `my_custom_check --foo ...`. The
127                // actual path is often too long to be worth showing
128                // in the IDE (e.g. in the VS Code status bar).
129                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/// Flycheck wraps the shared state and communication machinery used for
141/// running `cargo check` (or other compatible command) and providing
142/// diagnostics based on the output.
143/// The spawned thread is shut down when this struct is dropped.
144#[derive(Debug)]
145pub(crate) struct FlycheckHandle {
146    // XXX: drop order is significant
147    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    /// Schedule a re-start of the cargo check worker to do a workspace wide check.
183    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    /// Schedule a re-start of the cargo check worker to do a package wide check.
196    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    /// Stop this cargo check worker.
214    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    /// Request adding a diagnostic with fixes included to a file
241    AddDiagnostic {
242        id: usize,
243        generation: DiagnosticsGeneration,
244        workspace_root: Arc<AbsPathBuf>,
245        diagnostic: Diagnostic,
246        package_id: Option<Arc<PackageId>>,
247    },
248
249    /// Request clearing all outdated diagnostics.
250    ClearDiagnostics { id: usize, kind: ClearDiagnosticsKind },
251
252    /// Request check progress notification to client
253    Progress {
254        /// Flycheck instance ID
255        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
311/// A [`FlycheckActor`] is a single check instance of a workspace.
312struct FlycheckActor {
313    /// The workspace id of this flycheck instance.
314    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    /// Either the workspace root of the workspace we are flychecking,
322    /// or the project root of the project.
323    root: Arc<AbsPathBuf>,
324    sysroot_root: Option<AbsPathBuf>,
325    scope: FlycheckScope,
326    /// CargoHandle exists to wrap around the communication needed to be able to
327    /// run `cargo check` without blocking. Currently the Rust standard library
328    /// doesn't provide a way to read sub-process output without blocking, so we
329    /// have to wrap sub-processes output handling in a thread and pass messages
330    /// back over a channel.
331    command_handle: Option<CommandHandle<CargoCheckMessage>>,
332    /// The receiver side of the channel mentioned above.
333    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        // Biased to give restarts a preference so check outputs don't block a restart or stop
392        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                    // Cancel the previously spawned process
412                    self.cancel_check_process();
413                    while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
414                        // restart chained with a stop, so just cancel
415                        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                                // If `"rust-analyzer.cargo.targetDir": null`, we should use
444                                // workspace's target dir instead of hard-coded fallback.
445                                let target_dir = target_dir.as_deref().or(ws_target_dir);
446
447                                Some(
448                                    // As `CommandHandle::spawn`'s working directory is
449                                    // rust-analyzer's working directory, which might be different
450                                    // from the flycheck's working directory, we should canonicalize
451                                    // the output directory, otherwise we might write it into the
452                                    // wrong target dir.
453                                    // If `target_dir` is an absolute path, it will replace
454                                    // `self.root` and that's an intended behavior.
455                                    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                    // Watcher finished
483                    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                        // We finished without receiving any diagnostics.
498                        // Clear everything for good measure
499                        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                        // We clear diagnostics for packages on
521                        // `[CargoCheckMessage::CompilerArtifact]` but there seem to be setups where
522                        // cargo may not report an artifact to our runner at all. To handle such
523                        // cases, clear stale diagnostics when flycheck completes successfully.
524                        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        // If we rerun the thread, we need to discard the previous check results first
624        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    /// Construct a `Command` object for checking the user's code. If the user
646    /// has specified a custom command with placeholders that we cannot fill,
647    /// return None.
648    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                        // FIXME: &affected_workspace
710                        &*self.root
711                    }
712                };
713                let mut cmd = toolchain::command(command, root, extra_env);
714
715                // If the custom command has a $saved_file placeholder, and
716                // we're saving a file, replace the placeholder in the arguments.
717                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                            // The custom command has a $saved_file placeholder,
729                            // but we had an IDE event that wasn't a file save. Do nothing.
730                            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                // Skip certain kinds of messages to only spend time on what's useful
763                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}