Skip to main content

rust_analyzer_cli/
cargo.rs

1use crate::lsp::types::{CheckDiagnosticItem, CheckResponse};
2use anyhow::Result;
3use serde_json::Value;
4use std::path::Path;
5use tokio::process::Command;
6
7pub async fn run_cargo_check(workspace_root: &Path, target: Option<&str>) -> Result<CheckResponse> {
8    let mut cmd = Command::new("cargo");
9    cmd.arg("check").arg("--message-format=json");
10    if let Some(tgt) = target {
11        cmd.arg("--target").arg(tgt);
12    }
13    cmd.current_dir(workspace_root);
14
15    let output = cmd.output().await?;
16    let stdout_str = String::from_utf8_lossy(&output.stdout);
17    let stderr_str = String::from_utf8_lossy(&output.stderr);
18
19    let mut diagnostics = Vec::new();
20    for line in stdout_str.lines() {
21        if let Ok(v) = serde_json::from_str::<Value>(line)
22            && v.get("reason").and_then(|r| r.as_str()) == Some("compiler-message")
23            && let Some(msg_obj) = v.get("message")
24        {
25            let level = msg_obj
26                .get("level")
27                .and_then(|l| l.as_str())
28                .unwrap_or("info")
29                .to_string();
30            let message = msg_obj
31                .get("message")
32                .and_then(|m| m.as_str())
33                .unwrap_or("")
34                .to_string();
35
36            let mut file = None;
37            let mut line_num = None;
38            let mut col_num = None;
39
40            if let Some(spans) = msg_obj.get("spans").and_then(|s| s.as_array())
41                && let Some(primary) = spans
42                    .iter()
43                    .find(|s| s.get("is_primary").and_then(|p| p.as_bool()) == Some(true))
44            {
45                file = primary
46                    .get("file_name")
47                    .and_then(|f| f.as_str())
48                    .map(String::from);
49                line_num = primary
50                    .get("line_start")
51                    .and_then(|l| l.as_u64())
52                    .map(|n| n as u32);
53                col_num = primary
54                    .get("column_start")
55                    .and_then(|c| c.as_u64())
56                    .map(|n| n as u32);
57            }
58
59            diagnostics.push(CheckDiagnosticItem {
60                message,
61                level,
62                file,
63                line: line_num,
64                col: col_num,
65            });
66        }
67    }
68
69    append_cargo_failure_diagnostic(&mut diagnostics, output.status.success(), &stderr_str);
70
71    Ok(CheckResponse {
72        success: output.status.success(),
73        diagnostics,
74    })
75}
76
77fn append_cargo_failure_diagnostic(
78    diagnostics: &mut Vec<CheckDiagnosticItem>,
79    success: bool,
80    stderr: &str,
81) {
82    // Invalid targets and Cargo startup failures often produce no JSON compiler message.
83    if !success && diagnostics.is_empty() && !stderr.trim().is_empty() {
84        diagnostics.push(CheckDiagnosticItem {
85            message: format!("cargo check failed: {}", stderr.trim()),
86            level: "error".to_string(),
87            file: None,
88            line: None,
89            col: None,
90        });
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_cargo_failure_keeps_stderr_when_no_json_diagnostics_exist() {
100        let mut diagnostics = Vec::new();
101        append_cargo_failure_diagnostic(&mut diagnostics, false, "error: unknown target\n");
102        assert_eq!(diagnostics.len(), 1);
103        assert_eq!(diagnostics[0].level, "error");
104        assert!(diagnostics[0].message.contains("unknown target"));
105    }
106
107    #[test]
108    fn test_cargo_failure_does_not_duplicate_compiler_diagnostics() {
109        let mut diagnostics = vec![CheckDiagnosticItem {
110            message: "compiler error".to_string(),
111            level: "error".to_string(),
112            file: Some("src/main.rs".to_string()),
113            line: Some(1),
114            col: Some(1),
115        }];
116        append_cargo_failure_diagnostic(&mut diagnostics, false, "cargo failed");
117        assert_eq!(diagnostics.len(), 1);
118    }
119}