use std::fmt::{self, Write}; use crate::{ diagnostic_chain::DiagnosticChain, protocol::Diagnostic, ReportHandler, Severity, SourceCode, }; /** [`ReportHandler`] that renders JSON output. It's a machine-readable output. */ #[derive(Debug, Clone)] pub struct JSONReportHandler; impl JSONReportHandler { /// Create a new [`JSONReportHandler`]. There are no customization /// options. pub const fn new() -> Self { Self } } impl Default for JSONReportHandler { fn default() -> Self { Self::new() } } struct Escape<'a>(&'a str); impl fmt::Display for Escape<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for c in self.0.chars() { let escape = match c { '\\' => Some(r"\\"), '"' => Some(r#"\""#), '\r' => Some(r"\r"), '\n' => Some(r"\n"), '\t' => Some(r"\t"), '\u{08}' => Some(r"\b"), '\u{0c}' => Some(r"\f"), _ => None, }; if let Some(escape) = escape { f.write_str(escape)?; } else { f.write_char(c)?; } } Ok(()) } } const fn escape(input: &'_ str) -> Escape<'_> { Escape(input) } impl JSONReportHandler { /// Render a [`Diagnostic`]. This function is mostly internal and meant to /// be called by the toplevel [`ReportHandler`] handler, but is made public /// to make it easier (possible) to test in isolation from global state. pub fn render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result { self._render_report(f, diagnostic, None) } fn _render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), parent_src: Option<&dyn SourceCode>, ) -> fmt::Result { write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?; if let Some(code) = diagnostic.code() { write!(f, r#""code": "{}","#, escape(&code.to_string()))?; } let severity = match diagnostic.severity() { Some(Severity::Error) | None => "error", Some(Severity::Warning) => "warning", Some(Severity::Advice) => "advice", }; write!(f, r#""severity": "{:}","#, severity)?; if let Some(cause_iter) = diagnostic .diagnostic_source() .map(DiagnosticChain::from_diagnostic) .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) { write!(f, r#""causes": ["#)?; let mut add_comma = false; for error in cause_iter { if add_comma { write!(f, ",")?; } else { add_comma = true; } write!(f, r#""{}""#, escape(&error.to_string()))?; } write!(f, "],")? } else { write!(f, r#""causes": [],"#)?; } if let Some(url) = diagnostic.url() { write!(f, r#""url": "{}","#, &url.to_string())?; } if let Some(help) = diagnostic.help() { write!(f, r#""help": "{}","#, escape(&help.to_string()))?; } let src = diagnostic.source_code().or(parent_src); if let Some(src) = src { self.render_snippets(f, diagnostic, src)?; } if let Some(labels) = diagnostic.labels() { write!(f, r#""labels": ["#)?; let mut add_comma = false; for label in labels { if add_comma { write!(f, ",")?; } else { add_comma = true; } write!(f, "{{")?; if let Some(label_name) = label.label() { write!(f, r#""label": "{}","#, escape(label_name))?; } write!(f, r#""span": {{"#)?; write!(f, r#""offset": {},"#, label.offset())?; write!(f, r#""length": {}"#, label.len())?; write!(f, "}}}}")?; } write!(f, "],")?; } else { write!(f, r#""labels": [],"#)?; } if let Some(relateds) = diagnostic.related() { write!(f, r#""related": ["#)?; let mut add_comma = false; for related in relateds { if add_comma { write!(f, ",")?; } else { add_comma = true; } self._render_report(f, related, src)?; } write!(f, "]")?; } else { write!(f, r#""related": []"#)?; } write!(f, "}}") } fn render_snippets( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), source: &dyn SourceCode, ) -> fmt::Result { if let Some(mut labels) = diagnostic.labels() { if let Some(label) = labels.next() { if let Ok(span_content) = source.read_span(label.inner(), 0, 0) { let filename = span_content.name().unwrap_or_default(); return write!(f, r#""filename": "{}","#, escape(filename)); } } } write!(f, r#""filename": "","#) } } impl ReportHandler for JSONReportHandler { fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { self.render_report(f, diagnostic) } } #[test] fn test_escape() { assert_eq!(escape("a\nb").to_string(), r"a\nb"); assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette"); }