rust-analyzer-cli 0.5.0

A library and CLI tool built on top of rust-analyzer for codebase navigation
Documentation
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,
) {
    // Invalid targets and Cargo startup failures often produce no JSON compiler message.
    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);
    }
}