Skip to main content

espforge_diagnostics/
json.rs

1//! The machine format: one JSON document per run, stable field names.
2//!
3//! Spans are byte offsets into the original source, as `[start, end)`, and the
4//! file name is repeated per diagnostic so a consumer can annotate output
5//! without knowing which file the tool read.
6
7use serde_json::{Value, json};
8
9use crate::Diagnostic;
10
11/// Serialize diagnostics for `file_path` as pretty JSON.
12pub fn to_json(file_path: &str, diagnostics: &[Diagnostic]) -> String {
13    serde_json::to_string_pretty(
14        &json!({ "diagnostics": diagnostics_value(file_path, diagnostics) }),
15    )
16    .expect("a serde_json::Value is always serializable")
17}
18
19/// The diagnostics array, for callers composing a larger document
20/// (`check --format json` adds the file name and the board).
21pub fn diagnostics_value(file_path: &str, diagnostics: &[Diagnostic]) -> Value {
22    Value::Array(
23        diagnostics
24            .iter()
25            .map(|d| diagnostic_value(file_path, d))
26            .collect(),
27    )
28}
29
30fn diagnostic_value(file_path: &str, d: &Diagnostic) -> Value {
31    json!({
32        "code": d.code.as_str(),
33        "severity": d.severity.as_str(),
34        "message": d.message,
35        "primary": {
36            "file": file_path,
37            "span": [d.primary.start, d.primary.end],
38        },
39        "labels": d
40            .labels
41            .iter()
42            .map(|label| json!({
43                "span": [label.span.start, label.span.end],
44                "text": label.text,
45            }))
46            .collect::<Vec<_>>(),
47        "help": d.help,
48        "fixes": d
49            .fixes
50            .iter()
51            .map(|fix| json!({
52                "title": fix.title,
53                "edits": fix
54                    .edits
55                    .iter()
56                    .map(|edit| json!({
57                        "span": [edit.span.start, edit.span.end],
58                        "text": edit.text,
59                    }))
60                    .collect::<Vec<_>>(),
61            }))
62            .collect::<Vec<_>>(),
63    })
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69    use crate::{DiagCode, Edit, Fix, Label, Severity};
70
71    #[test]
72    fn the_document_has_the_documented_shape() {
73        let d = Diagnostic::error(
74            DiagCode::new("E0701"),
75            4..8,
76            "pin conflict",
77            "GPIO18 is taken",
78        )
79        .with_help("pick another pin");
80
81        let json = to_json("blink.espf", &[d]);
82        let value: Value = serde_json::from_str(&json).unwrap();
83        let diag = &value["diagnostics"][0];
84
85        assert_eq!(diag["code"], "E0701");
86        assert_eq!(diag["severity"], "error");
87        assert_eq!(diag["message"], "pin conflict");
88        assert_eq!(diag["primary"]["file"], "blink.espf");
89        assert_eq!(diag["primary"]["span"], json!([4, 8]));
90        assert_eq!(diag["labels"][0]["text"], "GPIO18 is taken");
91        assert_eq!(diag["help"], "pick another pin");
92        assert_eq!(diag["fixes"], json!([]));
93    }
94
95    #[test]
96    fn every_diagnostic_carries_a_code() {
97        let d = Diagnostic::error(DiagCode::new("E0101"), 0..1, "m", "l");
98        let json = to_json("x.espf", &[d]);
99        let value: Value = serde_json::from_str(&json).unwrap();
100        assert_eq!(value["diagnostics"][0]["code"], "E0101");
101    }
102
103    #[test]
104    fn fixes_and_context_labels_are_preserved() {
105        let d = Diagnostic {
106            code: DiagCode::new("E0701"),
107            severity: Severity::Warning,
108            message: "pin conflict".into(),
109            primary: 4..8,
110            labels: vec![
111                Label {
112                    span: 4..8,
113                    text: "claimed here".into(),
114                },
115                Label {
116                    span: 20..24,
117                    text: "first claimed here".into(),
118                },
119            ],
120            help: None,
121            fixes: vec![Fix {
122                title: "rename the second led".into(),
123                edits: vec![Edit {
124                    span: 24..27,
125                    text: "red-led-2".into(),
126                }],
127            }],
128        };
129
130        let json = to_json("blink.espf", &[d]);
131        let value: Value = serde_json::from_str(&json).unwrap();
132        let diag = &value["diagnostics"][0];
133
134        assert_eq!(diag["severity"], "warning");
135        assert_eq!(diag["labels"][1]["span"], json!([20, 24]));
136        assert_eq!(diag["fixes"][0]["title"], "rename the second led");
137        assert_eq!(diag["fixes"][0]["edits"][0]["span"], json!([24, 27]));
138        assert_eq!(diag["fixes"][0]["edits"][0]["text"], "red-led-2");
139        assert!(diag["help"].is_null());
140    }
141
142    #[test]
143    fn the_pretty_shape_is_pinned() {
144        let d = Diagnostic::error(DiagCode::new("E0701"), 0..1, "boom", "here");
145        assert_eq!(
146            to_json("x.espf", &[d]),
147            "{
148  \"diagnostics\": [
149    {
150      \"code\": \"E0701\",
151      \"fixes\": [],
152      \"help\": null,
153      \"labels\": [
154        {
155          \"span\": [
156            0,
157            1
158          ],
159          \"text\": \"here\"
160        }
161      ],
162      \"message\": \"boom\",
163      \"primary\": {
164        \"file\": \"x.espf\",
165        \"span\": [
166          0,
167          1
168        ]
169      },
170      \"severity\": \"error\"
171    }
172  ]
173}"
174        );
175    }
176}