1use serde::{Deserialize, Serialize};
37
38use crate::{Applicability, Diagnostic, Severity, SourceMap};
39
40#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct JsonDiagnostic {
43 pub message: String,
45
46 #[serde(skip_serializing_if = "Option::is_none")]
48 pub code: Option<String>,
49
50 pub severity: JsonSeverity,
52
53 pub spans: Vec<JsonSpan>,
55
56 #[serde(skip_serializing_if = "Vec::is_empty")]
58 pub notes: Vec<String>,
59
60 #[serde(skip_serializing_if = "Vec::is_empty")]
62 pub suggestions: Vec<JsonSuggestion>,
63
64 #[serde(skip_serializing_if = "Vec::is_empty")]
66 pub children: Vec<JsonDiagnostic>,
67}
68
69#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
71#[serde(rename_all = "lowercase")]
72pub enum JsonSeverity {
73 Bug,
75 Error,
77 Warning,
79 Note,
81 Help,
83}
84
85impl From<Severity> for JsonSeverity {
86 fn from(severity: Severity) -> Self {
87 match severity {
88 Severity::Bug => Self::Bug,
89 Severity::Error => Self::Error,
90 Severity::Warning => Self::Warning,
91 Severity::Note => Self::Note,
92 Severity::Help => Self::Help,
93 }
94 }
95}
96
97#[derive(Clone, Debug, Serialize, Deserialize)]
99pub struct JsonSpan {
100 pub file: String,
102
103 pub line_start: usize,
105
106 pub line_end: usize,
108
109 pub column_start: usize,
111
112 pub column_end: usize,
114
115 pub is_primary: bool,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub label: Option<String>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub text: Option<String>,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize)]
129pub struct JsonSuggestion {
130 pub message: String,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub replacement: Option<String>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub span: Option<JsonSpan>,
140
141 pub applicability: JsonApplicability,
143}
144
145#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
147#[serde(rename_all = "snake_case")]
148pub enum JsonApplicability {
149 MachineApplicable,
151 MaybeIncorrect,
153 HasPlaceholders,
155 Unspecified,
157}
158
159impl From<Applicability> for JsonApplicability {
160 fn from(applicability: Applicability) -> Self {
161 match applicability {
162 Applicability::MachineApplicable => Self::MachineApplicable,
163 Applicability::MaybeIncorrect => Self::MaybeIncorrect,
164 Applicability::HasPlaceholders => Self::HasPlaceholders,
165 Applicability::Unspecified => Self::Unspecified,
166 }
167 }
168}
169
170pub fn diagnostic_to_json(diagnostic: &Diagnostic, source_map: &SourceMap) -> JsonDiagnostic {
172 let spans = diagnostic
173 .labels
174 .iter()
175 .filter_map(|label| {
176 let file = source_map.get_file(label.span.file)?;
177 let span_info = file.span_lines(label.span.span);
178
179 Some(JsonSpan {
180 file: file.name.clone(),
181 line_start: span_info.start_line,
182 line_end: span_info.end_line,
183 column_start: span_info.start_col,
184 column_end: span_info.end_col,
185 is_primary: label.primary,
186 label: if label.message.is_empty() {
187 None
188 } else {
189 Some(label.message.clone())
190 },
191 text: if label.span.span.is_dummy() {
192 None
193 } else {
194 Some(file.source_text(label.span.span).to_string())
195 },
196 })
197 })
198 .collect();
199
200 let suggestions = diagnostic
201 .suggestions
202 .iter()
203 .map(|s| {
204 let span = source_map.get_file(s.span.file).map(|file| {
205 let span_info = file.span_lines(s.span.span);
206 JsonSpan {
207 file: file.name.clone(),
208 line_start: span_info.start_line,
209 line_end: span_info.end_line,
210 column_start: span_info.start_col,
211 column_end: span_info.end_col,
212 is_primary: true,
213 label: None,
214 text: None,
215 }
216 });
217
218 JsonSuggestion {
219 message: s.message.clone(),
220 replacement: if s.replacement.is_empty() {
221 None
222 } else {
223 Some(s.replacement.clone())
224 },
225 span,
226 applicability: s.applicability.into(),
227 }
228 })
229 .collect();
230
231 JsonDiagnostic {
232 message: diagnostic.message.clone(),
233 code: diagnostic.code.clone(),
234 severity: diagnostic.severity.into(),
235 spans,
236 notes: diagnostic.notes.clone(),
237 suggestions,
238 children: Vec::new(),
239 }
240}
241
242#[must_use]
244pub fn diagnostics_to_json(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Vec<JsonDiagnostic> {
245 diagnostics
246 .iter()
247 .map(|d| diagnostic_to_json(d, source_map))
248 .collect()
249}
250
251pub fn to_json_string(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Result<String, serde_json::Error> {
253 let json_diags = diagnostics_to_json(diagnostics, source_map);
254 serde_json::to_string_pretty(&json_diags)
255}
256
257pub fn to_json_lines(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Result<String, serde_json::Error> {
259 let mut output = String::new();
260 for diag in diagnostics {
261 let json_diag = diagnostic_to_json(diag, source_map);
262 output.push_str(&serde_json::to_string(&json_diag)?);
263 output.push('\n');
264 }
265 Ok(output)
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use bhc_span::{FileId, FullSpan, Span};
272
273 fn create_test_source_map() -> SourceMap {
274 let mut sm = SourceMap::new();
275 sm.add_file(
276 "test.hs".to_string(),
277 "module Test where\n\nfoo :: Int -> Int\nfoo x = x + y\n".to_string(),
278 );
279 sm
280 }
281
282 #[test]
283 fn test_json_diagnostic() {
284 let sm = create_test_source_map();
285
286 let diag = Diagnostic::error("undefined variable `y`")
287 .with_code("E0001")
288 .with_label(
289 FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
290 "not found in this scope",
291 )
292 .with_note("consider defining `y`");
293
294 let json_diag = diagnostic_to_json(&diag, &sm);
295
296 assert_eq!(json_diag.message, "undefined variable `y`");
297 assert_eq!(json_diag.code, Some("E0001".to_string()));
298 assert_eq!(json_diag.severity, JsonSeverity::Error);
299 assert_eq!(json_diag.spans.len(), 1);
300 assert_eq!(json_diag.spans[0].file, "test.hs");
301 assert!(json_diag.spans[0].is_primary);
302 assert_eq!(json_diag.notes.len(), 1);
303 }
304
305 #[test]
306 fn test_json_serialization() {
307 let sm = create_test_source_map();
308
309 let diag = Diagnostic::error("test error")
310 .with_code("E0001")
311 .with_label(
312 FullSpan::new(FileId::new(0), Span::from_raw(0, 6)),
313 "here",
314 );
315
316 let json_str = to_json_string(&[diag], &sm).unwrap();
317
318 assert!(json_str.contains("\"message\": \"test error\""));
319 assert!(json_str.contains("\"severity\": \"error\""));
320 assert!(json_str.contains("\"code\": \"E0001\""));
321 }
322
323 #[test]
324 fn test_json_lines() {
325 let sm = create_test_source_map();
326
327 let diag1 = Diagnostic::error("error 1");
328 let diag2 = Diagnostic::warning("warning 1");
329
330 let output = to_json_lines(&[diag1, diag2], &sm).unwrap();
331 let lines: Vec<&str> = output.lines().collect();
332
333 assert_eq!(lines.len(), 2);
334 assert!(lines[0].contains("error 1"));
335 assert!(lines[1].contains("warning 1"));
336 }
337}