use crate::lsp::types::{CheckDiagnosticItem, CheckResponse};
use anyhow::Result;
use serde_json::Value;
use std::path::Path;
use tokio::process::Command;
pub async fn run_cargo_check(workspace_root: &Path, target: Option<&str>) -> Result<CheckResponse> {
let mut cmd = Command::new("cargo");
cmd.arg("check").arg("--message-format=json");
if let Some(tgt) = target {
cmd.arg("--target").arg(tgt);
}
cmd.current_dir(workspace_root);
let output = cmd.output().await?;
let stdout_str = String::from_utf8_lossy(&output.stdout);
let stderr_str = String::from_utf8_lossy(&output.stderr);
let mut diagnostics = Vec::new();
for line in stdout_str.lines() {
if let Ok(v) = serde_json::from_str::<Value>(line)
&& v.get("reason").and_then(|r| r.as_str()) == Some("compiler-message")
&& let Some(msg_obj) = v.get("message")
{
let level = msg_obj
.get("level")
.and_then(|l| l.as_str())
.unwrap_or("info")
.to_string();
let message = msg_obj
.get("message")
.and_then(|m| m.as_str())
.unwrap_or("")
.to_string();
let mut file = None;
let mut line_num = None;
let mut col_num = None;
if let Some(spans) = msg_obj.get("spans").and_then(|s| s.as_array())
&& let Some(primary) = spans
.iter()
.find(|s| s.get("is_primary").and_then(|p| p.as_bool()) == Some(true))
{
file = primary
.get("file_name")
.and_then(|f| f.as_str())
.map(String::from);
line_num = primary
.get("line_start")
.and_then(|l| l.as_u64())
.map(|n| n as u32);
col_num = primary
.get("column_start")
.and_then(|c| c.as_u64())
.map(|n| n as u32);
}
diagnostics.push(CheckDiagnosticItem {
message,
level,
file,
line: line_num,
col: col_num,
});
}
}
append_cargo_failure_diagnostic(&mut diagnostics, output.status.success(), &stderr_str);
Ok(CheckResponse {
success: output.status.success(),
diagnostics,
})
}
fn append_cargo_failure_diagnostic(
diagnostics: &mut Vec<CheckDiagnosticItem>,
success: bool,
stderr: &str,
) {
if !success && diagnostics.is_empty() && !stderr.trim().is_empty() {
diagnostics.push(CheckDiagnosticItem {
message: format!("cargo check failed: {}", stderr.trim()),
level: "error".to_string(),
file: None,
line: None,
col: None,
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cargo_failure_keeps_stderr_when_no_json_diagnostics_exist() {
let mut diagnostics = Vec::new();
append_cargo_failure_diagnostic(&mut diagnostics, false, "error: unknown target\n");
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].level, "error");
assert!(diagnostics[0].message.contains("unknown target"));
}
#[test]
fn test_cargo_failure_does_not_duplicate_compiler_diagnostics() {
let mut diagnostics = vec![CheckDiagnosticItem {
message: "compiler error".to_string(),
level: "error".to_string(),
file: Some("src/main.rs".to_string()),
line: Some(1),
col: Some(1),
}];
append_cargo_failure_diagnostic(&mut diagnostics, false, "cargo failed");
assert_eq!(diagnostics.len(), 1);
}
}