use std::{fmt, io, process::Command, time::Duration};
use crossbeam_channel::{select_biased, unbounded, Receiver, Sender};
use paths::{AbsPath, AbsPathBuf, Utf8PathBuf};
use rustc_hash::FxHashMap;
use serde::Deserialize;
pub(crate) use cargo_metadata::diagnostic::{
Applicability, Diagnostic, DiagnosticCode, DiagnosticLevel, DiagnosticSpan,
};
use toolchain::Tool;
use crate::command::{CommandHandle, ParseFromLine};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) enum InvocationStrategy {
Once,
#[default]
PerWorkspace,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct CargoOptions {
pub(crate) target_triples: Vec<String>,
pub(crate) all_targets: bool,
pub(crate) no_default_features: bool,
pub(crate) all_features: bool,
pub(crate) features: Vec<String>,
pub(crate) extra_args: Vec<String>,
pub(crate) extra_test_bin_args: Vec<String>,
pub(crate) extra_env: FxHashMap<String, String>,
pub(crate) target_dir: Option<Utf8PathBuf>,
}
#[derive(Clone)]
pub(crate) enum Target {
Bin(String),
Example(String),
Benchmark(String),
Test(String),
}
impl CargoOptions {
pub(crate) fn apply_on_command(&self, cmd: &mut Command) {
for target in &self.target_triples {
cmd.args(["--target", target.as_str()]);
}
if self.all_targets {
cmd.arg("--all-targets");
}
if self.all_features {
cmd.arg("--all-features");
} else {
if self.no_default_features {
cmd.arg("--no-default-features");
}
if !self.features.is_empty() {
cmd.arg("--features");
cmd.arg(self.features.join(" "));
}
}
if let Some(target_dir) = &self.target_dir {
cmd.arg("--target-dir").arg(target_dir);
}
cmd.envs(&self.extra_env);
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum FlycheckConfig {
CargoCommand {
command: String,
options: CargoOptions,
ansi_color_output: bool,
},
CustomCommand {
command: String,
args: Vec<String>,
extra_env: FxHashMap<String, String>,
invocation_strategy: InvocationStrategy,
},
}
impl fmt::Display for FlycheckConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FlycheckConfig::CargoCommand { command, .. } => write!(f, "cargo {command}"),
FlycheckConfig::CustomCommand { command, args, .. } => {
write!(f, "{command} {}", args.join(" "))
}
}
}
}
#[derive(Debug)]
pub(crate) struct FlycheckHandle {
sender: Sender<StateChange>,
_thread: stdx::thread::JoinHandle,
id: usize,
}
impl FlycheckHandle {
pub(crate) fn spawn(
id: usize,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
sysroot_root: Option<AbsPathBuf>,
workspace_root: AbsPathBuf,
manifest_path: Option<AbsPathBuf>,
) -> FlycheckHandle {
let actor =
FlycheckActor::new(id, sender, config, sysroot_root, workspace_root, manifest_path);
let (sender, receiver) = unbounded::<StateChange>();
let thread = stdx::thread::Builder::new(stdx::thread::ThreadIntent::Worker)
.name("Flycheck".to_owned())
.spawn(move || actor.run(receiver))
.expect("failed to spawn thread");
FlycheckHandle { id, sender, _thread: thread }
}
pub(crate) fn restart_workspace(&self, saved_file: Option<AbsPathBuf>) {
self.sender.send(StateChange::Restart { package: None, saved_file, target: None }).unwrap();
}
pub(crate) fn restart_for_package(&self, package: String, target: Option<Target>) {
self.sender
.send(StateChange::Restart { package: Some(package), saved_file: None, target })
.unwrap();
}
pub(crate) fn cancel(&self) {
self.sender.send(StateChange::Cancel).unwrap();
}
pub(crate) fn id(&self) -> usize {
self.id
}
}
pub(crate) enum FlycheckMessage {
AddDiagnostic { id: usize, workspace_root: AbsPathBuf, diagnostic: Diagnostic },
ClearDiagnostics { id: usize },
Progress {
id: usize,
progress: Progress,
},
}
impl fmt::Debug for FlycheckMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FlycheckMessage::AddDiagnostic { id, workspace_root, diagnostic } => f
.debug_struct("AddDiagnostic")
.field("id", id)
.field("workspace_root", workspace_root)
.field("diagnostic_code", &diagnostic.code.as_ref().map(|it| &it.code))
.finish(),
FlycheckMessage::ClearDiagnostics { id } => {
f.debug_struct("ClearDiagnostics").field("id", id).finish()
}
FlycheckMessage::Progress { id, progress } => {
f.debug_struct("Progress").field("id", id).field("progress", progress).finish()
}
}
}
}
#[derive(Debug)]
pub(crate) enum Progress {
DidStart,
DidCheckCrate(String),
DidFinish(io::Result<()>),
DidCancel,
DidFailToRestart(String),
}
enum StateChange {
Restart { package: Option<String>, saved_file: Option<AbsPathBuf>, target: Option<Target> },
Cancel,
}
struct FlycheckActor {
id: usize,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
manifest_path: Option<AbsPathBuf>,
root: AbsPathBuf,
sysroot_root: Option<AbsPathBuf>,
command_handle: Option<CommandHandle<CargoCheckMessage>>,
command_receiver: Option<Receiver<CargoCheckMessage>>,
status: FlycheckStatus,
}
#[allow(clippy::large_enum_variant)]
enum Event {
RequestStateChange(StateChange),
CheckEvent(Option<CargoCheckMessage>),
}
#[derive(PartialEq)]
enum FlycheckStatus {
Started,
DiagnosticSent,
Finished,
}
pub(crate) const SAVED_FILE_PLACEHOLDER: &str = "$saved_file";
impl FlycheckActor {
fn new(
id: usize,
sender: Sender<FlycheckMessage>,
config: FlycheckConfig,
sysroot_root: Option<AbsPathBuf>,
workspace_root: AbsPathBuf,
manifest_path: Option<AbsPathBuf>,
) -> FlycheckActor {
tracing::info!(%id, ?workspace_root, "Spawning flycheck");
FlycheckActor {
id,
sender,
config,
sysroot_root,
root: workspace_root,
manifest_path,
command_handle: None,
command_receiver: None,
status: FlycheckStatus::Finished,
}
}
fn report_progress(&self, progress: Progress) {
self.send(FlycheckMessage::Progress { id: self.id, progress });
}
fn next_event(&self, inbox: &Receiver<StateChange>) -> Option<Event> {
let Some(command_receiver) = &self.command_receiver else {
return inbox.recv().ok().map(Event::RequestStateChange);
};
select_biased! {
recv(inbox) -> msg => msg.ok().map(Event::RequestStateChange),
recv(command_receiver) -> msg => Some(Event::CheckEvent(msg.ok())),
}
}
fn run(mut self, inbox: Receiver<StateChange>) {
'event: while let Some(event) = self.next_event(&inbox) {
match event {
Event::RequestStateChange(StateChange::Cancel) => {
tracing::debug!(flycheck_id = self.id, "flycheck cancelled");
self.cancel_check_process();
}
Event::RequestStateChange(StateChange::Restart { package, saved_file, target }) => {
self.cancel_check_process();
while let Ok(restart) = inbox.recv_timeout(Duration::from_millis(50)) {
if let StateChange::Cancel = restart {
continue 'event;
}
}
let Some(command) =
self.check_command(package.as_deref(), saved_file.as_deref(), target)
else {
continue;
};
let formatted_command = format!("{command:?}");
tracing::debug!(?command, "will restart flycheck");
let (sender, receiver) = unbounded();
match CommandHandle::spawn(command, sender) {
Ok(command_handle) => {
tracing::debug!(command = formatted_command, "did restart flycheck");
self.command_handle = Some(command_handle);
self.command_receiver = Some(receiver);
self.report_progress(Progress::DidStart);
self.status = FlycheckStatus::Started;
}
Err(error) => {
self.report_progress(Progress::DidFailToRestart(format!(
"Failed to run the following command: {formatted_command} error={error}"
)));
self.status = FlycheckStatus::Finished;
}
}
}
Event::CheckEvent(None) => {
tracing::debug!(flycheck_id = self.id, "flycheck finished");
let command_handle = self.command_handle.take().unwrap();
self.command_receiver.take();
let formatted_handle = format!("{command_handle:?}");
let res = command_handle.join();
if let Err(error) = &res {
tracing::error!(
"Flycheck failed to run the following command: {}, error={}",
formatted_handle,
error
);
}
if self.status == FlycheckStatus::Started {
self.send(FlycheckMessage::ClearDiagnostics { id: self.id });
}
self.report_progress(Progress::DidFinish(res));
self.status = FlycheckStatus::Finished;
}
Event::CheckEvent(Some(message)) => match message {
CargoCheckMessage::CompilerArtifact(msg) => {
tracing::trace!(
flycheck_id = self.id,
artifact = msg.target.name,
"artifact received"
);
self.report_progress(Progress::DidCheckCrate(msg.target.name));
}
CargoCheckMessage::Diagnostic(msg) => {
tracing::trace!(
flycheck_id = self.id,
message = msg.message,
"diagnostic received"
);
if self.status == FlycheckStatus::Started {
self.send(FlycheckMessage::ClearDiagnostics { id: self.id });
}
self.send(FlycheckMessage::AddDiagnostic {
id: self.id,
workspace_root: self.root.clone(),
diagnostic: msg,
});
self.status = FlycheckStatus::DiagnosticSent;
}
},
}
}
self.cancel_check_process();
}
fn cancel_check_process(&mut self) {
if let Some(command_handle) = self.command_handle.take() {
tracing::debug!(
command = ?command_handle,
"did cancel flycheck"
);
command_handle.cancel();
self.command_receiver.take();
self.report_progress(Progress::DidCancel);
self.status = FlycheckStatus::Finished;
}
}
fn check_command(
&self,
package: Option<&str>,
saved_file: Option<&AbsPath>,
target: Option<Target>,
) -> Option<Command> {
match &self.config {
FlycheckConfig::CargoCommand { command, options, ansi_color_output } => {
let mut cmd = Command::new(Tool::Cargo.path());
if let Some(sysroot_root) = &self.sysroot_root {
cmd.env("RUSTUP_TOOLCHAIN", AsRef::<std::path::Path>::as_ref(sysroot_root));
}
cmd.arg(command);
cmd.current_dir(&self.root);
match package {
Some(pkg) => cmd.arg("-p").arg(pkg),
None => cmd.arg("--workspace"),
};
if let Some(tgt) = target {
match tgt {
Target::Bin(tgt) => cmd.arg("--bin").arg(tgt),
Target::Example(tgt) => cmd.arg("--example").arg(tgt),
Target::Test(tgt) => cmd.arg("--test").arg(tgt),
Target::Benchmark(tgt) => cmd.arg("--bench").arg(tgt),
};
}
cmd.arg(if *ansi_color_output {
"--message-format=json-diagnostic-rendered-ansi"
} else {
"--message-format=json"
});
if let Some(manifest_path) = &self.manifest_path {
cmd.arg("--manifest-path");
cmd.arg(manifest_path);
if manifest_path.extension().map_or(false, |ext| ext == "rs") {
cmd.arg("-Zscript");
}
}
cmd.arg("--keep-going");
options.apply_on_command(&mut cmd);
cmd.args(&options.extra_args);
Some(cmd)
}
FlycheckConfig::CustomCommand { command, args, extra_env, invocation_strategy } => {
let mut cmd = Command::new(command);
cmd.envs(extra_env);
match invocation_strategy {
InvocationStrategy::Once => {
cmd.current_dir(&self.root);
}
InvocationStrategy::PerWorkspace => {
cmd.current_dir(&self.root);
}
}
if let Some(saved_file) = saved_file {
for arg in args {
if arg == SAVED_FILE_PLACEHOLDER {
cmd.arg(saved_file);
} else {
cmd.arg(arg);
}
}
} else {
for arg in args {
if arg == SAVED_FILE_PLACEHOLDER {
return None;
}
cmd.arg(arg);
}
}
Some(cmd)
}
}
}
#[track_caller]
fn send(&self, check_task: FlycheckMessage) {
self.sender.send(check_task).unwrap();
}
}
#[allow(clippy::large_enum_variant)]
enum CargoCheckMessage {
CompilerArtifact(cargo_metadata::Artifact),
Diagnostic(Diagnostic),
}
impl ParseFromLine for CargoCheckMessage {
fn from_line(line: &str, error: &mut String) -> Option<Self> {
let mut deserializer = serde_json::Deserializer::from_str(line);
deserializer.disable_recursion_limit();
if let Ok(message) = JsonMessage::deserialize(&mut deserializer) {
return match message {
JsonMessage::Cargo(message) => match message {
cargo_metadata::Message::CompilerArtifact(artifact) if !artifact.fresh => {
Some(CargoCheckMessage::CompilerArtifact(artifact))
}
cargo_metadata::Message::CompilerMessage(msg) => {
Some(CargoCheckMessage::Diagnostic(msg.message))
}
_ => None,
},
JsonMessage::Rustc(message) => Some(CargoCheckMessage::Diagnostic(message)),
};
}
error.push_str(line);
error.push('\n');
None
}
fn from_eof() -> Option<Self> {
None
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum JsonMessage {
Cargo(cargo_metadata::Message),
Rustc(Diagnostic),
}