use serde::{Deserialize, Serialize};
use crate::{Applicability, Diagnostic, Severity, SourceMap};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JsonDiagnostic {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub severity: JsonSeverity,
pub spans: Vec<JsonSpan>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub suggestions: Vec<JsonSuggestion>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<JsonDiagnostic>,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum JsonSeverity {
Bug,
Error,
Warning,
Note,
Help,
}
impl From<Severity> for JsonSeverity {
fn from(severity: Severity) -> Self {
match severity {
Severity::Bug => Self::Bug,
Severity::Error => Self::Error,
Severity::Warning => Self::Warning,
Severity::Note => Self::Note,
Severity::Help => Self::Help,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JsonSpan {
pub file: String,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
pub is_primary: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct JsonSuggestion {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub replacement: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<JsonSpan>,
pub applicability: JsonApplicability,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum JsonApplicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
Unspecified,
}
impl From<Applicability> for JsonApplicability {
fn from(applicability: Applicability) -> Self {
match applicability {
Applicability::MachineApplicable => Self::MachineApplicable,
Applicability::MaybeIncorrect => Self::MaybeIncorrect,
Applicability::HasPlaceholders => Self::HasPlaceholders,
Applicability::Unspecified => Self::Unspecified,
}
}
}
pub fn diagnostic_to_json(diagnostic: &Diagnostic, source_map: &SourceMap) -> JsonDiagnostic {
let spans = diagnostic
.labels
.iter()
.filter_map(|label| {
let file = source_map.get_file(label.span.file)?;
let span_info = file.span_lines(label.span.span);
Some(JsonSpan {
file: file.name.clone(),
line_start: span_info.start_line,
line_end: span_info.end_line,
column_start: span_info.start_col,
column_end: span_info.end_col,
is_primary: label.primary,
label: if label.message.is_empty() {
None
} else {
Some(label.message.clone())
},
text: if label.span.span.is_dummy() {
None
} else {
Some(file.source_text(label.span.span).to_string())
},
})
})
.collect();
let suggestions = diagnostic
.suggestions
.iter()
.map(|s| {
let span = source_map.get_file(s.span.file).map(|file| {
let span_info = file.span_lines(s.span.span);
JsonSpan {
file: file.name.clone(),
line_start: span_info.start_line,
line_end: span_info.end_line,
column_start: span_info.start_col,
column_end: span_info.end_col,
is_primary: true,
label: None,
text: None,
}
});
JsonSuggestion {
message: s.message.clone(),
replacement: if s.replacement.is_empty() {
None
} else {
Some(s.replacement.clone())
},
span,
applicability: s.applicability.into(),
}
})
.collect();
JsonDiagnostic {
message: diagnostic.message.clone(),
code: diagnostic.code.clone(),
severity: diagnostic.severity.into(),
spans,
notes: diagnostic.notes.clone(),
suggestions,
children: Vec::new(),
}
}
#[must_use]
pub fn diagnostics_to_json(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Vec<JsonDiagnostic> {
diagnostics
.iter()
.map(|d| diagnostic_to_json(d, source_map))
.collect()
}
pub fn to_json_string(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Result<String, serde_json::Error> {
let json_diags = diagnostics_to_json(diagnostics, source_map);
serde_json::to_string_pretty(&json_diags)
}
pub fn to_json_lines(diagnostics: &[Diagnostic], source_map: &SourceMap) -> Result<String, serde_json::Error> {
let mut output = String::new();
for diag in diagnostics {
let json_diag = diagnostic_to_json(diag, source_map);
output.push_str(&serde_json::to_string(&json_diag)?);
output.push('\n');
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use bhc_span::{FileId, FullSpan, Span};
fn create_test_source_map() -> SourceMap {
let mut sm = SourceMap::new();
sm.add_file(
"test.hs".to_string(),
"module Test where\n\nfoo :: Int -> Int\nfoo x = x + y\n".to_string(),
);
sm
}
#[test]
fn test_json_diagnostic() {
let sm = create_test_source_map();
let diag = Diagnostic::error("undefined variable `y`")
.with_code("E0001")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(50, 51)),
"not found in this scope",
)
.with_note("consider defining `y`");
let json_diag = diagnostic_to_json(&diag, &sm);
assert_eq!(json_diag.message, "undefined variable `y`");
assert_eq!(json_diag.code, Some("E0001".to_string()));
assert_eq!(json_diag.severity, JsonSeverity::Error);
assert_eq!(json_diag.spans.len(), 1);
assert_eq!(json_diag.spans[0].file, "test.hs");
assert!(json_diag.spans[0].is_primary);
assert_eq!(json_diag.notes.len(), 1);
}
#[test]
fn test_json_serialization() {
let sm = create_test_source_map();
let diag = Diagnostic::error("test error")
.with_code("E0001")
.with_label(
FullSpan::new(FileId::new(0), Span::from_raw(0, 6)),
"here",
);
let json_str = to_json_string(&[diag], &sm).unwrap();
assert!(json_str.contains("\"message\": \"test error\""));
assert!(json_str.contains("\"severity\": \"error\""));
assert!(json_str.contains("\"code\": \"E0001\""));
}
#[test]
fn test_json_lines() {
let sm = create_test_source_map();
let diag1 = Diagnostic::error("error 1");
let diag2 = Diagnostic::warning("warning 1");
let output = to_json_lines(&[diag1, diag2], &sm).unwrap();
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 2);
assert!(lines[0].contains("error 1"));
assert!(lines[1].contains("warning 1"));
}
}