use std::fmt;
use miette::{
Diagnostic, GraphicalReportHandler, GraphicalTheme, JSONReportHandler, NamedSource, Severity,
SourceCode, SourceSpan,
};
use serde_json::Value;
use svugy_model::problem::ErrorSeverity;
use svugy_model::validate::ModelReport;
pub type BoxedDiagnostic = Box<dyn Diagnostic + Send + Sync>;
#[derive(Debug)]
pub struct InputReport {
input_name: String,
importer_diagnostics: Vec<BoxedDiagnostic>,
source_code: Option<NamedSource<String>>,
model_report: Option<ModelReport>,
}
impl InputReport {
pub fn new(input_name: String, importer_diagnostics: Vec<BoxedDiagnostic>) -> Self {
Self {
input_name,
importer_diagnostics,
source_code: None,
model_report: None,
}
}
pub fn with_source_code(mut self, source_code: NamedSource<String>) -> Self {
self.source_code = Some(source_code);
self
}
pub fn with_model_report(mut self, model_report: ModelReport) -> Self {
self.model_report = Some(model_report);
self
}
pub fn eprint_diagnostics(&self) {
let is_clean = self.diagnostics().next().is_none();
if is_clean {
return;
}
let handler = GraphicalReportHandler::new();
let rendered = self.render_graphical(&handler);
eprint!("{rendered}");
}
pub fn to_plain_text(&self) -> String {
let plain_theme = GraphicalTheme::unicode_nocolor();
let handler = GraphicalReportHandler::new_themed(plain_theme);
self.render_graphical(&handler)
}
fn render_graphical(&self, handler: &GraphicalReportHandler) -> String {
let mut rendered = String::new();
handler
.render_report(&mut rendered, self)
.expect("every span points into the source it is rendered against");
rendered
}
pub fn to_json(&self) -> String {
let handler = JSONReportHandler::new();
let mut miette_json = String::new();
handler
.render_report(&mut miette_json, self)
.expect("a JSON report renders into a string: unreadable spans are skipped");
let Some(source_code) = &self.source_code else {
miette_json.push('\n');
return miette_json;
};
let mut json_report: Value =
serde_json::from_str(&miette_json).expect("miette renders valid JSON");
Self::locate_every_label(&mut json_report, source_code);
let mut located_json = json_report.to_string();
located_json.push('\n');
located_json
}
fn locate_every_label(json_report: &mut Value, source_code: &NamedSource<String>) {
let related_diagnostics = json_report["related"].as_array_mut().into_iter().flatten();
let labels = related_diagnostics
.flat_map(|diagnostic| diagnostic["labels"].as_array_mut())
.flatten();
for label in labels {
let span_offset = label["span"]["offset"].as_u64();
let span_length = label["span"]["length"].as_u64();
let (Some(span_offset), Some(span_length)) = (span_offset, span_length) else {
continue;
};
let span = SourceSpan::from((span_offset as usize, span_length as usize));
let Ok(span_contents) = source_code.read_span(&span, 0, 0) else {
continue;
};
let line = span_contents.line() + 1;
let column = span_contents.column() + 1;
label["line"] = Value::from(line);
label["column"] = Value::from(column);
}
}
pub fn has_errors(&self) -> bool {
self.diagnostics().any(|diagnostic| diagnostic.is_error())
}
fn diagnostics(&self) -> impl Iterator<Item = &dyn Diagnostic> {
let importer_diagnostics = self
.importer_diagnostics
.iter()
.map(|diagnostic| diagnostic.as_ref() as &dyn Diagnostic);
let model_problems = self
.model_report
.iter()
.flat_map(ModelReport::problems)
.map(|problem| problem as &dyn Diagnostic);
importer_diagnostics.chain(model_problems)
}
}
impl fmt::Display for InputReport {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let input_name = &self.input_name;
let error_count = self
.diagnostics()
.filter(|diagnostic| diagnostic.is_error())
.count();
let diagnostic_count = self.diagnostics().count();
let warning_count = diagnostic_count - error_count;
write!(
formatter,
"{input_name}: {error_count} error(s), {warning_count} warning(s)"
)
}
}
impl std::error::Error for InputReport {}
impl Diagnostic for InputReport {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
let code: Box<dyn fmt::Display + 'a> = Box::new("svugy::input_report");
Some(code)
}
fn severity(&self) -> Option<Severity> {
let has_errors = self.has_errors();
if has_errors {
return Some(Severity::Error);
}
let is_clean = self.diagnostics().next().is_none();
if is_clean {
return Some(Severity::Advice);
}
Some(Severity::Warning)
}
fn source_code(&self) -> Option<&dyn SourceCode> {
let source_code = self.source_code.as_ref()?;
Some(source_code)
}
fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
let diagnostics = self.diagnostics();
let related: Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a> = Box::new(diagnostics);
Some(related)
}
}