blob: 29e21a0d63f016ae5367cf639e93fa17a16a6fce [file] [log] [blame]
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");
}