Skip to main content

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 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/// Data needed to construct a `cargo` command invocation, e.g. for flycheck or running a test.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub(crate) struct CargoOptions {
43    /// The cargo subcommand to run, e.g. "check" or "clippy"
44    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                // No --benches unfortunately, as this implies --tests (see https://github.com/rust-lang/cargo/issues/6454),
75                // and users setting `cfg.seTest = false` probably prefer disabling benches than enabling tests.
76                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/// The flycheck config from a rust-project.json file or discoverConfig JSON output.
97#[derive(Debug, Default)]
98pub(crate) struct FlycheckConfigJson {
99    /// The template with [project_json::RunnableKind::Flycheck]
100    pub single_template: Option<project_json::Runnable>,
101}
102
103impl FlycheckConfigJson {
104    pub(crate) fn any_configured(&self) -> bool {
105        // self.workspace_template.is_some() ||
106        self.single_template.is_some()
107    }
108}
109
110/// The flycheck config from rust-analyzer's own configuration.
111///
112/// We rely on this when rust-project.json does not specify a flycheck runnable
113///
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub(crate) enum FlycheckConfig {
116    /// Automatically use rust-project.json's flycheck runnable or just use cargo (the common case)
117    ///
118    /// We can't have a variant for ProjectJson because that is configured on the fly during
119    /// discoverConfig. We only know what we can read at config time.
120    Automatic {
121        /// If we do use cargo, how to build the check command
122        cargo_options: CargoOptions,
123        ansi_color_output: bool,
124    },
125    /// check_overrideCommand. This overrides both cargo and rust-project.json's flycheck runnable.
126    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    /// Show a shortened version of the check command.
147    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                // Don't show `my_custom_check --foo $saved_file` literally to the user, as it
154                // looks like we've forgotten to substitute $saved_file.
155                //
156                // `my_custom_check --foo /home/user/project/src/dir/foo.rs` is too verbose.
157                //
158                // Instead, show `my_custom_check --foo ...`. The
159                // actual path is often too long to be worth showing
160                // in the IDE (e.g. in the VS Code status bar).
161                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/// Flycheck wraps the shared state and communication machinery used for
182/// running `cargo check` (or other compatible command) and providing
183/// diagnostics based on the output.
184/// The spawned thread is shut down when this struct is dropped.
185#[derive(Debug)]
186pub(crate) struct FlycheckHandle {
187    // XXX: drop order is significant
188    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    /// Schedule a re-start of the cargo check worker to do a workspace wide check.
226    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    /// Schedule a re-start of the cargo check worker to do a package wide check.
239    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    /// Stop this cargo check worker.
258    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    /// Request adding a diagnostic with fixes included to a file
285    AddDiagnostic {
286        id: usize,
287        generation: DiagnosticsGeneration,
288        workspace_root: Arc<AbsPathBuf>,
289        diagnostic: Diagnostic,
290        package_id: Option<PackageSpecifier>,
291    },
292
293    /// Request clearing all outdated diagnostics.
294    ClearDiagnostics { id: usize, kind: ClearDiagnosticsKind },
295
296    /// Request check progress notification to client
297    Progress {
298        /// Flycheck instance ID
299        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        /// The user sees this in VSCode, etc. May be a shortened version of the command we actually
335        /// executed, otherwise it is way too long.
336        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        // Either a cargo package or a $label in rust-project.check.overrideCommand
349        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        /// The one in Cargo.toml, assumed to work with `cargo check -p {}` etc
358        package_id: Arc<PackageId>,
359    },
360    BuildInfo {
361        /// If a `build` field is present in rust-project.json, its label field
362        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    /// Regular cargo invocation
378    Cargo,
379    /// Configured via check_overrideCommand
380    CheckOverrideCommand,
381    /// From a runnable with [project_json::RunnableKind::Flycheck]
382    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
396/// A [`FlycheckActor`] is a single check instance of a workspace.
397struct FlycheckActor {
398    /// The workspace id of this flycheck instance.
399    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    /// Either the workspace root of the workspace we are flychecking,
409    /// or the project root of the project.
410    root: Arc<AbsPathBuf>,
411    sysroot_root: Option<AbsPathBuf>,
412    scope: FlycheckScope,
413    /// CargoHandle exists to wrap around the communication needed to be able to
414    /// run `cargo check` without blocking. Currently the Rust standard library
415    /// doesn't provide a way to read sub-process output without blocking, so we
416    /// have to wrap sub-processes output handling in a thread and pass messages
417    /// back over a channel.
418    command_handle: Option<CommandHandle<CheckMessage>>,
419    /// The receiver side of the channel mentioned above.
420    command_receiver: Option<Receiver<CheckMessage>>,
421    diagnostics_cleared_for: FxHashSet<PackageSpecifier>,
422    diagnostics_received: DiagnosticsReceived,
423}
424
425#[derive(PartialEq, Debug)]
426enum DiagnosticsReceived {
427    /// We started a flycheck, but we haven't seen any diagnostics yet.
428    NotYet,
429    /// We received a non-zero number of diagnostics from rustc or clippy (via
430    /// cargo or custom check command). This means there were errors or
431    /// warnings.
432    AtLeastOne,
433    /// We received a non-zero number of diagnostics, and the scope is
434    /// workspace, so we've discarded the previous workspace diagnostics.
435    AtLeastOneAndClearedWorkspace,
436}
437
438#[allow(clippy::large_enum_variant)]
439#[derive(Debug)]
440enum Event {
441    RequestStateChange(StateChange),
442    CheckEvent(Option<CheckMessage>),
443}
444
445/// This is stable behaviour. Don't change.
446const 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    /// If you have a runnable, and it has {label} in it somewhere, treat it as a template that
458    /// may be unsatisfied if you do not provide a label to substitute into it. Returns None in
459    /// that situation. Otherwise performs the requested substitutions.
460    ///
461    /// Same for {saved_file}.
462    ///
463    #[allow(clippy::disallowed_types)] /* generic parameter allows for FxHashMap */
464    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            // Legacy syntax: full argument match
492            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        // Biased to give restarts a preference so check outputs don't block a restart or stop
548        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                    // Cancel the previously spawned process
568                    self.cancel_check_process();
569
570                    // Debounce by briefly waiting for other state changes.
571                    while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
572                        match restart {
573                            StateChange::Cancel => {
574                                // We got a cancel straight after this restart request, so
575                                // don't do anything.
576                                continue 'event;
577                            }
578                            StateChange::Restart {
579                                generation: g,
580                                scope: s,
581                                saved_file: sf,
582                                target: t,
583                            } => {
584                                // We got another restart request. Take the parameters
585                                // from the last restart request in this time window,
586                                // because the most recent request is probably the most
587                                // relevant to the user.
588                                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                                // If `"rust-analyzer.cargo.targetDir": null`, we should use
622                                // workspace's target dir instead of hard-coded fallback.
623                                let target_dir = target_dir.as_deref().or(ws_target_dir);
624
625                                Some(
626                                    // As `CommandHandle::spawn`'s working directory is
627                                    // rust-analyzer's working directory, which might be different
628                                    // from the flycheck's working directory, we should canonicalize
629                                    // the output directory, otherwise we might write it into the
630                                    // wrong target dir.
631                                    // If `target_dir` is an absolute path, it will replace
632                                    // `self.root` and that's an intended behavior.
633                                    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                    // Watcher finished
661                    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                        // We finished without receiving any diagnostics.
676                        //
677                        // `cargo check` generally outputs something, even if there are no
678                        // warnings/errors, so we always know which package was checked.
679                        //
680                        // ```text
681                        // $ cargo check --message-format=json 2>/dev/null
682                        // {"reason":"compiler-artifact","package_id":"path+file:///Users/wilfred/tmp/scratch#0.1.0",...}
683                        // ```
684                        //
685                        // However, rustc only returns JSON if there are diagnostics present, so a
686                        // build without warnings or errors has an empty output.
687                        //
688                        // ```
689                        // $ rustc --error-format=json bad.rs
690                        // {"$message_type":"diagnostic","message":"mismatched types","...}
691                        //
692                        // $ rustc --error-format=json good.rs
693                        // ```
694                        //
695                        // So if we got zero diagnostics, it was almost certainly a check that
696                        // wasn't specific to a package.
697                        self.send(FlycheckMessage::ClearDiagnostics {
698                            id: self.id,
699                            kind: ClearDiagnosticsKind::All(ClearScope::Workspace),
700                        });
701                    } else if res.is_ok() {
702                        // We clear diagnostics for packages on
703                        // `[CargoCheckMessage::CompilerArtifact]` but there seem to be setups where
704                        // cargo may not report an artifact to our runner at all. To handle such
705                        // cases, clear stale diagnostics when flycheck completes successfully.
706                        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        // If we rerun the thread, we need to discard the previous check results first
812        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            // We could add a runnable like "RunnableKind::FlycheckWorkspace". But generally
840            // if you're not running cargo, it's because your workspace is too big to check
841            // all at once. You can always use `check_overrideCommand` with no {label}.
842            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    /// Construct a `Command` object for checking the user's code. If the user
857    /// has specified a custom command with placeholders that we cannot fill,
858    /// return None.
859    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                // Only use the rust-project.json's flycheck config when no check_overrideCommand
868                // is configured. In the FlycheckConcig::CustomCommand branch we will still do
869                // label substitution, but on the overrideCommand instead.
870                //
871                // There needs to be SOME way to override what your discoverConfig tool says,
872                // because to change the flycheck runnable there you may have to literally
873                // recompile the tool.
874                if self.config_json.any_configured() {
875                    // Completely handle according to rust-project.json.
876                    // We don't consider this to be "using cargo" so we will not apply any of the
877                    // CargoOptions to the command.
878                    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                        // No way to flycheck this single package. All we have is a build label.
903                        // There's no way to really say whether this build label happens to be
904                        // a cargo canonical name, so we won't try.
905                        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                        // FIXME: &affected_workspace
947                        &*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                    // We support substituting both build labels (e.g. buck, bazel) and cargo package ids.
960                    // With cargo package ids, you get `cargo check -p path+file:///path/to/rust-analyzer/crates/hir#0.0.0`.
961                    // That does work!
962                    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    /// A message from `cargo check`, including details like the path
983    /// to the relevant `Cargo.toml`.
984    CompilerArtifact(cargo_metadata::Artifact),
985    /// A diagnostic message from rustc itself.
986    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                // Skip certain kinds of messages to only spend time on what's useful
998                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        // Runnable says it needs both; you need both.
1049        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        // Only need label? only need label.
1061        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        // Only need saved_file
1070        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        // Need neither
1082        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        // {label} mid-argument substitution
1091        assert_eq!(
1092            test_substitute(Some(label), None, "--label={label}").as_deref(),
1093            Some("build --label=:label")
1094        );
1095
1096        // {saved_file} mid-argument substitution
1097        assert_eq!(
1098            test_substitute(None, Some(saved_file), "--saved={saved_file}").as_deref(),
1099            Some("build --saved=file.rs")
1100        );
1101
1102        // $saved_file legacy support (no mid-argument substitution, we never supported that)
1103        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}