ra_ap_flycheck/
lib.rs

1//! Flycheck provides the functionality needed to run `cargo check` or
2//! another compatible command (f.x. clippy) in a background thread and provide
3//! LSP diagnostics based on the output of the command.
4
5// FIXME: This crate now handles running `cargo test` needed in the test explorer in
6// addition to `cargo check`. Either split it into 3 crates (one for test, one for check
7// and one common utilities) or change its name and docs to reflect the current state.
8
9use std::{fmt, io, process::Command, time::Duration};
10
11use crossbeam_channel::{never, select, unbounded, Receiver, Sender};
12use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
13use rustc_hash::FxHashMap;
14use serde::Deserialize;
15
16pub use cargo_metadata::diagnostic::{
17    Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
18    DiagnosticSpanMacroExpansion,
19};
20use toolchain::Tool;
21
22mod command;
23pub mod project_json;
24mod test_runner;
25
26use command::{CommandHandle, ParseFromLine};
27pub use test_runner::{CargoTestHandle, CargoTestMessage, TestState, TestTarget};
28
29#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
30pub enum InvocationStrategy {
31    Once,
32    #[default]
33    PerWorkspace,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
37pub enum InvocationLocation {
38    Root(AbsPathBuf),
39    #[default]
40    Workspace,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub struct CargoOptions {
45    pub target_triples: Vec<String>,
46    pub all_targets: bool,
47    pub no_default_features: bool,
48    pub all_features: bool,
49    pub features: Vec<String>,
50    pub extra_args: Vec<String>,
51    pub extra_env: FxHashMap<String, String>,
52    pub target_dir: Option<Utf8PathBuf>,
53}
54
55impl CargoOptions {
56    fn apply_on_command(&self, cmd: &mut Command) {
57        for target in &self.target_triples {
58            cmd.args(["--target", target.as_str()]);
59        }
60        if self.all_targets {
61            cmd.arg("--all-targets");
62        }
63        if self.all_features {
64            cmd.arg("--all-features");
65        } else {
66            if self.no_default_features {
67                cmd.arg("--no-default-features");
68            }
69            if !self.features.is_empty() {
70                cmd.arg("--features");
71                cmd.arg(self.features.join(" "));
72            }
73        }
74        if let Some(target_dir) = &self.target_dir {
75            cmd.arg("--target-dir").arg(target_dir);
76        }
77        cmd.envs(&self.extra_env);
78    }
79}
80
81#[derive(Clone, Debug, PartialEq, Eq)]
82pub enum FlycheckConfig {
83    CargoCommand {
84        command: String,
85        options: CargoOptions,
86        ansi_color_output: bool,
87    },
88    CustomCommand {
89        command: String,
90        args: Vec<String>,
91        extra_env: FxHashMap<String, String>,
92        invocation_strategy: InvocationStrategy,
93        invocation_location: InvocationLocation,
94    },
95}
96
97impl fmt::Display for FlycheckConfig {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        match self {
100            FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
101            FlycheckConfig::CustomCommand { command, args, .. } => {
102                write!(f, "{command} {}", args.join(" "))
103            }
104        }
105    }
106}
107
108/// Flycheck wraps the shared state and communication machinery used for
109/// running `cargo check` (or other compatible command) and providing
110/// diagnostics based on the output.
111/// The spawned thread is shut down when this struct is dropped.
112#[derive(Debug)]
113pub struct FlycheckHandle {
114    // XXX: drop order is significant
115    sender: Sender<StateChange>,
116    _thread: stdx::thread::JoinHandle,
117    id: usize,
118}
119
120impl FlycheckHandle {
121    pub fn spawn(
122        id: usize,
123        sender: Box<dyn Fn(Message) + Send>,
124        config: FlycheckConfig,
125        sysroot_root: Option<AbsPathBuf>,
126        workspace_root: AbsPathBuf,
127        manifest_path: Option<AbsPathBuf>,
128    ) -> FlycheckHandle {
129        let actor =
130            FlycheckActor::new(id, sender, config, sysroot_root, workspace_root, manifest_path);
131        let (sender, receiver) = unbounded::<StateChange>();
132        let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
133            .name("Flycheck".to_owned())
134            .spawn(move || actor.run(receiver))
135            .expect("failed to spawn thread");
136        FlycheckHandle { id, sender, _thread: thread }
137    }
138
139    /// Schedule a re-start of the cargo check worker to do a workspace wide check.
140    pub fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
141        self.sender.send(StateChange::Restart { package: None, saved_file }).unwrap();
142    }
143
144    /// Schedule a re-start of the cargo check worker to do a package wide check.
145    pub fn restart_for_package(&self, package: String) {
146        self.sender
147            .send(StateChange::Restart { package: Some(package), saved_file: None })
148            .unwrap();
149    }
150
151    /// Stop this cargo check worker.
152    pub fn cancel(&self) {
153        self.sender.send(StateChange::Cancel).unwrap();
154    }
155
156    pub fn id(&self) -> usize {
157        self.id
158    }
159}
160
161pub enum Message {
162    /// Request adding a diagnostic with fixes included to a file
163    AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
164
165    /// Request clearing all previous diagnostics
166    ClearDiagnostics { id: usize },
167
168    /// Request check progress notification to client
169    Progress {
170        /// Flycheck instance ID
171        id: usize,
172        progress: Progress,
173    },
174}
175
176impl fmt::Debug for Message {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        match self {
179            Message::AddDiagnostic { id, workspace_root, diagnostic } => f
180                .debug_struct("AddDiagnostic")
181                .field("id", id)
182                .field("workspace_root", workspace_root)
183                .field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
184                .finish(),
185            Message::ClearDiagnostics { id } => {
186                f.debug_struct("ClearDiagnostics").field("id", id).finish()
187            }
188            Message::Progress { id, progress } => {
189                f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
190            }
191        }
192    }
193}
194
195#[derive(Debug)]
196pub enum Progress {
197    DidStart,
198    DidCheckCrate(String),
199    DidFinish(io::Result<()>),
200    DidCancel,
201    DidFailToRestart(String),
202}
203
204enum StateChange {
205    Restart { package: Option<String>, saved_file: Option<AbsPathBuf> },
206    Cancel,
207}
208
209/// A [`FlycheckActor`] is a single check instance of a workspace.
210struct FlycheckActor {
211    /// The workspace id of this flycheck instance.
212    id: usize,
213    sender: Box<dyn Fn(Message) + Send>,
214    config: FlycheckConfig,
215    manifest_path: Option<AbsPathBuf>,
216    /// Either the workspace root of the workspace we are flychecking,
217    /// or the project root of the project.
218    root: AbsPathBuf,
219    sysroot_root: Option<AbsPathBuf>,
220    /// CargoHandle exists to wrap around the communication needed to be able to
221    /// run `cargo check` without blocking. Currently the Rust standard library
222    /// doesn't provide a way to read sub-process output without blocking, so we
223    /// have to wrap sub-processes output handling in a thread and pass messages
224    /// back over a channel.
225    command_handle: Option<CommandHandle<CargoCheckMessage>>,
226    /// The receiver side of the channel mentioned above.
227    command_receiver: Option<Receiver<CargoCheckMessage>>,
228
229    status: FlycheckStatus,
230}
231
232enum Event {
233    RequestStateChange(StateChange),
234    CheckEvent(Option<CargoCheckMessage>),
235}
236
237#[derive(PartialEq)]
238enum FlycheckStatus {
239    Started,
240    DiagnosticSent,
241    Finished,
242}
243
244pub const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
245
246impl FlycheckActor {
247    fn new(
248        id: usize,
249        sender: Box<dyn Fn(Message) + Send>,
250        config: FlycheckConfig,
251        sysroot_root: Option<AbsPathBuf>,
252        workspace_root: AbsPathBuf,
253        manifest_path: Option<AbsPathBuf>,
254    ) -> FlycheckActor {
255        tracing::info!(%id, ?workspace_root, "Spawning flycheck");
256        FlycheckActor {
257            id,
258            sender,
259            config,
260            sysroot_root,
261            root: workspace_root,
262            manifest_path,
263            command_handle: None,
264            command_receiver: None,
265            status: FlycheckStatus::Finished,
266        }
267    }
268
269    fn report_progress(&self, progress: Progress) {
270        self.send(Message::Progress { id: self.id, progress });
271    }
272
273    fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
274        if let Ok(msg) = inbox.try_recv() {
275            // give restarts a preference so check outputs don't block a restart or stop
276            return Some(Event::RequestStateChange(msg));
277        }
278        select! {
279            recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
280            recv(self.command_receiver.as_ref().unwrap_or(&never())) -> msg => Some(Event::CheckEvent(msg.ok())),
281        }
282    }
283
284    fn run(mut self, inbox: Receiver<StateChange>) {
285        'event: while let Some(event) = self.next_event(&inbox) {
286            match event {
287                Event::RequestStateChange(StateChange::Cancel) => {
288                    tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
289                    self.cancel_check_process();
290                }
291                Event::RequestStateChange(StateChange::Restart { package, saved_file }) => {
292                    // Cancel the previously spawned process
293                    self.cancel_check_process();
294                    while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
295                        // restart chained with a stop, so just cancel
296                        if let StateChange::Cancel = restart {
297                            continue 'event;
298                        }
299                    }
300
301                    let command =
302                        match self.check_command(package.as_deref(), saved_file.as_deref()) {
303                            Some(c) => c,
304                            None => continue,
305                        };
306                    let formatted_command = format!("{command:?}");
307
308                    tracing::debug!(?command, "will restart flycheck");
309                    let (sender, receiver) = unbounded();
310                    match CommandHandle::spawn(command, sender) {
311                        Ok(command_handle) => {
312                            tracing::debug!(command = formatted_command, "did restart flycheck");
313                            self.command_handle = Some(command_handle);
314                            self.command_receiver = Some(receiver);
315                            self.report_progress(Progress::DidStart);
316                            self.status = FlycheckStatus::Started;
317                        }
318                        Err(error) => {
319                            self.report_progress(Progress::DidFailToRestart(format!(
320                                "Failed to run the following command: {formatted_command} error={error}"
321                            )));
322                            self.status = FlycheckStatus::Finished;
323                        }
324                    }
325                }
326                Event::CheckEvent(None) => {
327                    tracing::debug!(flycheck_id = self.id, "flycheck finished");
328
329                    // Watcher finished
330                    let command_handle = self.command_handle.take().unwrap();
331                    self.command_receiver.take();
332                    let formatted_handle = format!("{command_handle:?}");
333
334                    let res = command_handle.join();
335                    if let Err(error) = &res {
336                        tracing::error!(
337                            "Flycheck failed to run the following command: {}, error={}",
338                            formatted_handle,
339                            error
340                        );
341                    }
342                    if self.status == FlycheckStatus::Started {
343                        self.send(Message::ClearDiagnostics { id: self.id });
344                    }
345                    self.report_progress(Progress::DidFinish(res));
346                    self.status = FlycheckStatus::Finished;
347                }
348                Event::CheckEvent(Some(message)) => match message {
349                    CargoCheckMessage::CompilerArtifact(msg) => {
350                        tracing::trace!(
351                            flycheck_id = self.id,
352                            artifact = msg.target.name,
353                            "artifact received"
354                        );
355                        self.report_progress(Progress::DidCheckCrate(msg.target.name));
356                    }
357
358                    CargoCheckMessage::Diagnostic(msg) => {
359                        tracing::trace!(
360                            flycheck_id = self.id,
361                            message = msg.message,
362                            "diagnostic received"
363                        );
364                        if self.status == FlycheckStatus::Started {
365                            self.send(Message::ClearDiagnostics { id: self.id });
366                        }
367                        self.send(Message::AddDiagnostic {
368                            id: self.id,
369                            workspace_root: self.root.clone(),
370                            diagnostic: msg,
371                        });
372                        self.status = FlycheckStatus::DiagnosticSent;
373                    }
374                },
375            }
376        }
377        // If we rerun the thread, we need to discard the previous check results first
378        self.cancel_check_process();
379    }
380
381    fn cancel_check_process(&mut self) {
382        if let Some(command_handle) = self.command_handle.take() {
383            tracing::debug!(
384                command = ?command_handle,
385                "did  cancel flycheck"
386            );
387            command_handle.cancel();
388            self.command_receiver.take();
389            self.report_progress(Progress::DidCancel);
390            self.status = FlycheckStatus::Finished;
391        }
392    }
393
394    /// Construct a `Command` object for checking the user's code. If the user
395    /// has specified a custom command with placeholders that we cannot fill,
396    /// return None.
397    fn check_command(
398        &self,
399        package: Option<&str>,
400        saved_file: Option<&AbsPath>,
401    ) -> Option<Command> {
402        let (mut cmd, args) = match &self.config {
403            FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
404                let mut cmd = Command::new(Tool::Cargo.path());
405                if let Some(sysroot_root) = &self.sysroot_root {
406                    cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
407                }
408                cmd.arg(command);
409                cmd.current_dir(&self.root);
410
411                match package {
412                    Some(pkg) => cmd.arg("-p").arg(pkg),
413                    None => cmd.arg("--workspace"),
414                };
415
416                cmd.arg(if *ansi_color_output {
417                    "--message-format=json-diagnostic-rendered-ansi"
418                } else {
419                    "--message-format=json"
420                });
421
422                if let Some(manifest_path) = &self.manifest_path {
423                    cmd.arg("--manifest-path");
424                    cmd.arg(manifest_path);
425                    if manifest_path.extension().map_or(false, |ext| ext == "rs") {
426                        cmd.arg("-Zscript");
427                    }
428                }
429
430                cmd.arg("--keep-going");
431
432                options.apply_on_command(&mut cmd);
433                (cmd, options.extra_args.clone())
434            }
435            FlycheckConfig::CustomCommand {
436                command,
437                args,
438                extra_env,
439                invocation_strategy,
440                invocation_location,
441            } => {
442                let mut cmd = Command::new(command);
443                cmd.envs(extra_env);
444
445                match invocation_location {
446                    InvocationLocation::Workspace => {
447                        match invocation_strategy {
448                            InvocationStrategy::Once => {
449                                cmd.current_dir(&self.root);
450                            }
451                            InvocationStrategy::PerWorkspace => {
452                                // FIXME: cmd.current_dir(&affected_workspace);
453                                cmd.current_dir(&self.root);
454                            }
455                        }
456                    }
457                    InvocationLocation::Root(root) => {
458                        cmd.current_dir(root);
459                    }
460                }
461
462                if args.contains(&SAVED_FILE_PLACEHOLDER.to_owned()) {
463                    // If the custom command has a $saved_file placeholder, and
464                    // we're saving a file, replace the placeholder in the arguments.
465                    if let Some(saved_file) = saved_file {
466                        let args = args
467                            .iter()
468                            .map(|arg| {
469                                if arg == SAVED_FILE_PLACEHOLDER {
470                                    saved_file.to_string()
471                                } else {
472                                    arg.clone()
473                                }
474                            })
475                            .collect();
476                        (cmd, args)
477                    } else {
478                        // The custom command has a $saved_file placeholder,
479                        // but we had an IDE event that wasn't a file save. Do nothing.
480                        return None;
481                    }
482                } else {
483                    (cmd, args.clone())
484                }
485            }
486        };
487
488        cmd.args(args);
489        Some(cmd)
490    }
491
492    fn send(&self, check_task: Message) {
493        (self.sender)(check_task);
494    }
495}
496
497#[allow(clippy::large_enum_variant)]
498enum CargoCheckMessage {
499    CompilerArtifact(cargo_metadata::Artifact),
500    Diagnostic(Diagnostic),
501}
502
503impl ParseFromLine for CargoCheckMessage {
504    fn from_line(line: &str, error: &mut String) -> Option<Self> {
505        let mut deserializer = serde_json::Deserializer::from_str(line);
506        deserializer.disable_recursion_limit();
507        if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
508            return match message {
509                // Skip certain kinds of messages to only spend time on what's useful
510                JsonMessage::Cargo(message) => match message {
511                    cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
512                        Some(CargoCheckMessage::CompilerArtifact(artifact))
513                    }
514                    cargo_metadata::Message::CompilerMessage(msg) => {
515                        Some(CargoCheckMessage::Diagnostic(msg.message))
516                    }
517                    _ => None,
518                },
519                JsonMessage::Rustc(message) => Some(CargoCheckMessage::Diagnostic(message)),
520            };
521        }
522
523        error.push_str(line);
524        error.push('\n');
525        None
526    }
527
528    fn from_eof() -> Option<Self> {
529        None
530    }
531}
532
533#[derive(Deserialize)]
534#[serde(untagged)]
535enum JsonMessage {
536    Cargo(cargo_metadata::Message),
537    Rustc(Diagnostic),
538}