use std::collections::HashMap;
use std::io::{BufRead, BufReader};
use std::path::{Component, Path, PathBuf};
use std::process::{Command, Stdio};
use super::cli::CheckArgs;
struct Projected {
source: PathBuf,
line: usize,
level: String,
message: String,
}
pub(crate) fn run_check_cmd(args: CheckArgs) {
let mut cmd = Command::new("cargo");
cmd.arg("check").arg("--message-format=json");
if let Some(package) = &args.common.package {
cmd.arg("-p").arg(package);
}
if let Some(features) = &args.common.features {
cmd.arg("--features").arg(features);
}
if args.all_targets {
cmd.arg("--all-targets");
}
cmd.args(&args.common.cargo_args);
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => {
eprintln!("[cargo-telar] could not run cargo check: {e}");
std::process::exit(1);
}
};
let mut maps: HashMap<PathBuf, Option<Vec<Option<usize>>>> = HashMap::new();
let mut projected: Vec<Projected> = Vec::new();
let mut rendered_passthrough: Vec<String> = Vec::new();
if let Some(stdout) = child.stdout.take() {
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
let Ok(value) = serde_json::from_str::<serde_json::Value>(&line) else {
continue;
};
if value.get("reason").and_then(serde_json::Value::as_str) != Some("compiler-message") {
continue;
}
let Some(message) = value.get("message") else {
continue;
};
let level = message
.get("level")
.and_then(serde_json::Value::as_str)
.unwrap_or("error");
if level != "error" && level != "warning" {
continue;
}
let text = message
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let rendered = message
.get("rendered")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.to_string();
let mut mapped_any = false;
for span in primary_spans(message) {
let Some(file) = span.get("file_name").and_then(serde_json::Value::as_str) else {
continue;
};
let generated = PathBuf::from(file);
if !is_generated(&generated) {
continue;
}
let Some(line_start) = span.get("line_start").and_then(serde_json::Value::as_u64)
else {
continue;
};
let Some(source) = generated_to_source(&generated) else {
continue;
};
let map = maps
.entry(generated.clone())
.or_insert_with(|| read_line_map(&generated));
let Some(map) = map.as_ref() else { continue };
let Some(Some(rsx_line)) = map.get(line_start as usize - 1) else {
continue;
};
projected.push(Projected {
source,
line: rsx_line + 1,
level: level.to_string(),
message: text.clone(),
});
mapped_any = true;
}
if !mapped_any && !rendered.is_empty() {
rendered_passthrough.push(rendered);
}
}
}
let status = child.wait();
if !projected.is_empty() {
eprintln!();
eprintln!("[cargo-telar] diagnostics mapped back to their `.rsx` source:");
for item in &projected {
eprintln!(
" {}: {}:{}: {}",
item.level,
display(&item.source),
item.line,
item.message
);
}
eprintln!();
}
for rendered in &rendered_passthrough {
eprint!("{rendered}");
}
let code = status
.ok()
.and_then(|status| status.code())
.unwrap_or(if projected.is_empty() { 0 } else { 1 });
std::process::exit(code);
}
fn primary_spans(message: &serde_json::Value) -> Vec<&serde_json::Value> {
message
.get("spans")
.and_then(serde_json::Value::as_array)
.map(|spans| {
spans
.iter()
.filter(|span| {
span.get("is_primary")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false)
})
.collect()
})
.unwrap_or_default()
}
fn is_generated(path: &Path) -> bool {
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
return false;
}
build_root(path).is_some()
}
fn build_root(path: &Path) -> Option<usize> {
let parts: Vec<Component> = path.components().collect();
parts.iter().enumerate().position(|(i, part)| {
part.as_os_str() == ".telar"
&& matches!(
parts.get(i + 1).map(|part| part.as_os_str()),
Some(next) if next == "build" || next == "build-hot"
)
})
}
fn generated_to_source(generated: &Path) -> Option<PathBuf> {
let at = build_root(generated)?;
let parts: Vec<Component> = generated.components().collect();
let mut source: PathBuf = parts[..at].iter().collect();
source.push("src");
for part in &parts[at + 2..] {
source.push(part.as_os_str());
}
Some(source.with_extension("rsx"))
}
fn read_line_map(generated: &Path) -> Option<Vec<Option<usize>>> {
let mut path = generated.as_os_str().to_os_string();
path.push(".map");
let text = std::fs::read_to_string(PathBuf::from(path)).ok()?;
serde_json::from_str(&text).ok()
}
fn display(path: &Path) -> String {
std::env::current_dir()
.ok()
.and_then(|cwd| path.strip_prefix(cwd).ok().map(Path::to_path_buf))
.unwrap_or_else(|| path.to_path_buf())
.display()
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_generated_file_maps_back_to_its_rsx() {
let generated = PathBuf::from("/w/crates/modules/.telar/build/clock/clock.rs");
assert!(is_generated(&generated));
assert_eq!(
generated_to_source(&generated),
Some(PathBuf::from("/w/crates/modules/src/clock/clock.rsx"))
);
}
#[test]
fn a_hot_reload_build_dir_maps_too() {
let generated = PathBuf::from("/w/apps/a/.telar/build-hot/home.rs");
assert!(is_generated(&generated));
assert_eq!(
generated_to_source(&generated),
Some(PathBuf::from("/w/apps/a/src/home.rsx"))
);
}
#[test]
fn a_hand_written_rust_file_is_left_alone() {
assert!(!is_generated(Path::new("/w/crates/ui/src/icon/mod.rs")));
assert!(!is_generated(Path::new("/w/crates/ui/.telar/other/x.rs")));
}
}