adk-code 0.6.0

Code execution substrate for ADK-Rust — typed executor abstraction, sandbox policy model, and built-in execution backends
Documentation
//! Rust compiler diagnostic parsing.
//!
//! This module provides types and a parser for `rustc --error-format=json` output.
//! Each line of stderr from `rustc` in JSON mode is a JSON object containing
//! diagnostic information. This module parses those objects into structured
//! [`RustDiagnostic`] values.
//!
//! # Example
//!
//! ```rust
//! use adk_code::diagnostics::parse_diagnostics;
//!
//! let stderr = r#"{"message":"expected `;`","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"main.rs","byte_start":10,"byte_end":11,"line_start":1,"line_end":1,"column_start":11,"column_end":12,"is_primary":true,"text":[{"text":"let x = 1","highlight_start":11,"highlight_end":12}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error: expected `;`"}"#;
//! let diagnostics = parse_diagnostics(stderr);
//! assert_eq!(diagnostics.len(), 1);
//! assert_eq!(diagnostics[0].level, "error");
//! ```

use serde::{Deserialize, Serialize};

/// A parsed Rust compiler diagnostic.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RustDiagnostic {
    /// Severity level: `"error"`, `"warning"`, `"note"`, `"help"`.
    pub level: String,
    /// The diagnostic message.
    pub message: String,
    /// Source spans where the diagnostic applies.
    #[serde(default)]
    pub spans: Vec<DiagnosticSpan>,
    /// Optional error code (e.g., `"E0308"`).
    pub code: Option<String>,
}

/// A source span within a diagnostic.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticSpan {
    /// The file name where the diagnostic applies.
    pub file_name: String,
    /// Starting line number (1-indexed).
    pub line_start: u32,
    /// Ending line number (1-indexed).
    pub line_end: u32,
    /// Starting column number (1-indexed).
    pub column_start: u32,
    /// Ending column number (1-indexed).
    pub column_end: u32,
    /// Source text with highlight information.
    #[serde(default)]
    pub text: Vec<SpanText>,
}

/// A line of source text with highlight markers.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanText {
    /// The source text of the line.
    pub text: String,
    /// Column where the highlight starts (1-indexed).
    pub highlight_start: u32,
    /// Column where the highlight ends (1-indexed).
    pub highlight_end: u32,
}

/// Intermediate type for deserializing rustc JSON output.
///
/// The rustc JSON format nests the error code inside a `code` object:
/// `{"code": {"code": "E0308", "explanation": null}}`.
#[derive(Debug, Deserialize)]
struct RawDiagnostic {
    level: String,
    message: String,
    #[serde(default)]
    spans: Vec<RawSpan>,
    code: Option<RawCode>,
}

#[derive(Debug, Deserialize)]
struct RawCode {
    code: String,
}

#[derive(Debug, Deserialize)]
struct RawSpan {
    file_name: String,
    line_start: u32,
    line_end: u32,
    column_start: u32,
    column_end: u32,
    #[serde(default)]
    text: Vec<RawSpanText>,
}

#[derive(Debug, Deserialize)]
struct RawSpanText {
    text: String,
    highlight_start: u32,
    highlight_end: u32,
}

/// Parse rustc JSON diagnostics from `--error-format=json` output.
///
/// Each line of stderr is attempted as a JSON diagnostic object. Lines that
/// fail to parse (e.g., non-JSON rendered output) are silently skipped.
///
/// # Example
///
/// ```rust
/// use adk_code::diagnostics::parse_diagnostics;
///
/// let stderr = "";
/// let diagnostics = parse_diagnostics(stderr);
/// assert!(diagnostics.is_empty());
/// ```
pub fn parse_diagnostics(stderr: &str) -> Vec<RustDiagnostic> {
    stderr
        .lines()
        .filter_map(|line| {
            let line = line.trim();
            if line.is_empty() {
                return None;
            }
            let raw: RawDiagnostic = serde_json::from_str(line).ok()?;
            Some(RustDiagnostic {
                level: raw.level,
                message: raw.message,
                spans: raw
                    .spans
                    .into_iter()
                    .map(|s| DiagnosticSpan {
                        file_name: s.file_name,
                        line_start: s.line_start,
                        line_end: s.line_end,
                        column_start: s.column_start,
                        column_end: s.column_end,
                        text: s
                            .text
                            .into_iter()
                            .map(|t| SpanText {
                                text: t.text,
                                highlight_start: t.highlight_start,
                                highlight_end: t.highlight_end,
                            })
                            .collect(),
                    })
                    .collect(),
                code: raw.code.map(|c| c.code),
            })
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_empty_stderr() {
        let diagnostics = parse_diagnostics("");
        assert!(diagnostics.is_empty());
    }

    #[test]
    fn parse_non_json_lines_skipped() {
        let stderr = "some random text\nnot json at all\n";
        let diagnostics = parse_diagnostics(stderr);
        assert!(diagnostics.is_empty());
    }

    #[test]
    fn parse_single_error_diagnostic() {
        let stderr = r#"{"message":"expected `;`","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"main.rs","byte_start":10,"byte_end":11,"line_start":1,"line_end":1,"column_start":11,"column_end":12,"is_primary":true,"text":[{"text":"let x = 1","highlight_start":11,"highlight_end":12}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error: expected `;`"}"#;
        let diagnostics = parse_diagnostics(stderr);
        assert_eq!(diagnostics.len(), 1);
        assert_eq!(diagnostics[0].level, "error");
        assert_eq!(diagnostics[0].message, "expected `;`");
        assert_eq!(diagnostics[0].code.as_deref(), Some("E0308"));
        assert_eq!(diagnostics[0].spans.len(), 1);
        assert_eq!(diagnostics[0].spans[0].file_name, "main.rs");
        assert_eq!(diagnostics[0].spans[0].line_start, 1);
        assert_eq!(diagnostics[0].spans[0].column_start, 11);
        assert_eq!(diagnostics[0].spans[0].text.len(), 1);
        assert_eq!(diagnostics[0].spans[0].text[0].text, "let x = 1");
    }

    #[test]
    fn parse_warning_without_code() {
        let stderr = r#"{"message":"unused variable: `x`","code":null,"level":"warning","spans":[],"children":[],"rendered":"warning: unused variable"}"#;
        let diagnostics = parse_diagnostics(stderr);
        assert_eq!(diagnostics.len(), 1);
        assert_eq!(diagnostics[0].level, "warning");
        assert!(diagnostics[0].code.is_none());
        assert!(diagnostics[0].spans.is_empty());
    }

    #[test]
    fn parse_mixed_json_and_text() {
        let stderr = format!(
            "{}\nerror[E0308]: mismatched types\n{}",
            r#"{"message":"type mismatch","code":{"code":"E0308","explanation":null},"level":"error","spans":[],"children":[],"rendered":"error"}"#,
            r#"{"message":"help: consider","code":null,"level":"help","spans":[],"children":[],"rendered":"help"}"#,
        );
        let diagnostics = parse_diagnostics(&stderr);
        assert_eq!(diagnostics.len(), 2);
        assert_eq!(diagnostics[0].level, "error");
        assert_eq!(diagnostics[1].level, "help");
    }

    #[test]
    fn parse_diagnostic_with_multiple_spans() {
        let stderr = r#"{"message":"mismatched types","code":{"code":"E0308","explanation":null},"level":"error","spans":[{"file_name":"main.rs","byte_start":10,"byte_end":11,"line_start":1,"line_end":1,"column_start":11,"column_end":12,"is_primary":true,"text":[{"text":"let x: i32 = \"hello\"","highlight_start":15,"highlight_end":22}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null},{"file_name":"main.rs","byte_start":20,"byte_end":25,"line_start":2,"line_end":2,"column_start":5,"column_end":10,"is_primary":false,"text":[{"text":"    x + 1","highlight_start":5,"highlight_end":10}],"label":null,"suggested_replacement":null,"suggestion_applicability":null,"expansion":null}],"children":[],"rendered":"error"}"#;
        let diagnostics = parse_diagnostics(stderr);
        assert_eq!(diagnostics.len(), 1);
        assert_eq!(diagnostics[0].spans.len(), 2);
        assert_eq!(diagnostics[0].spans[0].line_start, 1);
        assert_eq!(diagnostics[0].spans[1].line_start, 2);
    }
}