Skip to main content

perspt_coding/diag/
cargo.rs

1//! `cargo --message-format=json` diagnostics (PSP-10 system 26).
2//!
3//! Preserves codes, primary spans, children, and applicable suggestions
4//! from the compiler's native JSON stream. Non-JSON lines are ignored, so
5//! mixed streams (human progress on stderr, JSON on stdout) parse cleanly.
6
7use perspt_sdk::ResidualClass;
8
9use super::types::StructuredDiagnostic;
10use crate::lang::classify_rust_code;
11
12/// Parser identity; enters the sensor fingerprint.
13pub const PARSER_ID: &str = "cargo-json-v1";
14
15/// Whether a captured output looks like a cargo JSON stream.
16pub fn looks_like_stream(raw: &str) -> bool {
17    raw.lines()
18        .any(|line| line.starts_with('{') && line.contains("\"reason\""))
19}
20
21/// Parse every `compiler-message` with level `error` from the stream.
22pub fn parse(raw: &str) -> Vec<StructuredDiagnostic> {
23    let mut diagnostics = Vec::new();
24    for line in raw.lines() {
25        if !line.starts_with('{') {
26            continue;
27        }
28        let Ok(value) = serde_json::from_str::<serde_json::Value>(line) else {
29            continue;
30        };
31        if value.get("reason").and_then(|r| r.as_str()) != Some("compiler-message") {
32            continue;
33        }
34        let Some(message) = value.get("message") else {
35            continue;
36        };
37        if message.get("level").and_then(|l| l.as_str()) != Some("error") {
38            continue;
39        }
40        diagnostics.push(from_message(message));
41    }
42    diagnostics
43}
44
45fn from_message(message: &serde_json::Value) -> StructuredDiagnostic {
46    let code = message
47        .get("code")
48        .and_then(|code| code.get("code"))
49        .and_then(|code| code.as_str())
50        .map(str::to_string);
51    let class = code
52        .as_deref()
53        .map(classify_rust_code)
54        .unwrap_or(ResidualClass::Build);
55    let span = message
56        .get("spans")
57        .and_then(|spans| spans.as_array())
58        .and_then(|spans| {
59            spans
60                .iter()
61                .find(|span| span.get("is_primary").and_then(|p| p.as_bool()) == Some(true))
62                .or_else(|| spans.first())
63        });
64    let suggestion = span
65        .and_then(|span| span.get("suggested_replacement"))
66        .and_then(|value| value.as_str())
67        .map(str::to_string)
68        .or_else(|| {
69            message
70                .get("children")
71                .and_then(|children| children.as_array())
72                .and_then(|children| {
73                    children.iter().find_map(|child| {
74                        child
75                            .get("spans")?
76                            .as_array()?
77                            .iter()
78                            .find_map(|span| span.get("suggested_replacement")?.as_str())
79                            .map(str::to_string)
80                    })
81                })
82        });
83    StructuredDiagnostic {
84        class,
85        code,
86        message: message
87            .get("message")
88            .and_then(|m| m.as_str())
89            .unwrap_or_default()
90            .to_string(),
91        path: span
92            .and_then(|span| span.get("file_name"))
93            .and_then(|f| f.as_str())
94            .map(str::to_string),
95        line: span
96            .and_then(|span| span.get("line_start"))
97            .and_then(|l| l.as_u64())
98            .and_then(|l| u32::try_from(l).ok()),
99        column: span
100            .and_then(|span| span.get("column_start"))
101            .and_then(|c| c.as_u64())
102            .and_then(|c| u32::try_from(c).ok()),
103        primary: true,
104        suggestion,
105    }
106}