oxc-miette 2.7.1

Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers.
Documentation
use std::fmt::{self, Write};

use crate::{
    ReportHandler, Severity, SourceCode, diagnostic_chain::DiagnosticChain, protocol::Diagnostic,
};

/**
[`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()))?;
        }
        if let Some(note) = diagnostic.note() {
            write!(f, r#""note": "{}","#, escape(&note.to_string()))?;
        }
        let src = diagnostic.source_code().or(parent_src);
        if let Some(src) = src {
            self.render_snippets(f, diagnostic, src)?;
        }
        match diagnostic.labels() {
            Some(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())?;

                    if let Some(Ok(location)) = diagnostic
                        .source_code()
                        .or(parent_src)
                        .map(|src| src.read_span(label.inner(), 0, 0))
                    {
                        write!(f, r#""line": {},"#, location.line() + 1)?;
                        write!(f, r#""column": {}"#, location.column() + 1)?;
                    } else {
                        write!(f, r#""line": null,"column": null"#)?;
                    }

                    write!(f, "}}}}")?;
                }
                write!(f, "],")?;
            }
            _ => {
                write!(f, r#""labels": [],"#)?;
            }
        }
        match diagnostic.related() {
            Some(relates) => {
                write!(f, r#""related": ["#)?;
                let mut add_comma = false;
                for related in relates {
                    if add_comma {
                        write!(f, ",")?;
                    } else {
                        add_comma = true;
                    }
                    self._render_report(f, related, src)?;
                }
                write!(f, "]")?;
            }
            _ => {
                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");
}