1use std::fmt::{self, Write};
2
3use crate::{Severity, protocol::Diagnostic, source_impls::SpanScanner};
4
5#[derive(Debug, Clone)]
9pub struct JSONReportHandler;
10
11impl JSONReportHandler {
12 #[must_use]
15 pub const fn new() -> Self {
16 Self
17 }
18}
19
20impl Default for JSONReportHandler {
21 fn default() -> Self {
22 Self::new()
23 }
24}
25
26struct Escape<'a>(&'a str);
27
28impl fmt::Display for Escape<'_> {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 for c in self.0.chars() {
31 let escape = match c {
32 '\\' => Some(r"\\"),
33 '"' => Some(r#"\""#),
34 '\r' => Some(r"\r"),
35 '\n' => Some(r"\n"),
36 '\t' => Some(r"\t"),
37 '\u{08}' => Some(r"\b"),
38 '\u{0c}' => Some(r"\f"),
39 _ => None,
40 };
41 if let Some(escape) = escape {
42 f.write_str(escape)?;
43 } else {
44 f.write_char(c)?;
45 }
46 }
47 Ok(())
48 }
49}
50
51const fn escape(input: &'_ str) -> Escape<'_> {
52 Escape(input)
53}
54
55impl JSONReportHandler {
56 #[expect(clippy::unused_self, reason = "keeps a consistent renderer API")]
62 pub fn render_report(
63 &self,
64 f: &mut impl fmt::Write,
65 diagnostic: &dyn Diagnostic,
66 ) -> fmt::Result {
67 write!(f, r#"{{"message": "{}","#, escape(&diagnostic.to_string()))?;
68 if let Some(code) = diagnostic.code() {
69 write!(f, r#""code": "{}","#, escape(&code))?;
70 }
71 let severity = match diagnostic.severity() {
72 Some(Severity::Error) | None => "error",
73 Some(Severity::Warning) => "warning",
74 Some(Severity::Advice) => "advice",
75 };
76 write!(f, r#""severity": "{severity:}","#)?;
77 if let Some(url) = diagnostic.url() {
78 write!(f, r#""url": "{url}","#)?;
79 }
80 if let Some(help) = diagnostic.help() {
81 write!(f, r#""help": "{}","#, escape(&help))?;
82 }
83 if let Some(note) = diagnostic.note() {
84 write!(f, r#""note": "{}","#, escape(¬e))?;
85 }
86 let source = diagnostic.source_code();
87 if let Some(source) = source {
88 write!(f, r#""filename": "{}","#, escape(source.name().unwrap_or_default()))?;
89 }
90 {
91 write!(f, r#""labels": ["#)?;
92 let mut scanner = source.map(|source| SpanScanner::new(source.data(), 0, 0));
93 let mut add_comma = false;
94 for label in diagnostic.labels() {
95 if add_comma {
96 write!(f, ",")?;
97 } else {
98 add_comma = true;
99 }
100 write!(f, "{{")?;
101 if let Some(label_name) = label.label() {
102 write!(f, r#""label": "{}","#, escape(label_name))?;
103 }
104 write!(f, r#""span": {{"#)?;
105 write!(f, r#""offset": {},"#, label.offset())?;
106 write!(f, r#""length": {},"#, label.len())?;
107
108 if let Some(location) =
109 scanner.as_mut().and_then(|scanner| scanner.read_span(*label.inner()))
110 {
111 write!(f, r#""line": {},"#, location.line() + 1)?;
112 write!(f, r#""column": {}"#, location.column() + 1)?;
113 } else {
114 write!(f, r#""line": null,"column": null"#)?;
115 }
116
117 write!(f, "}}}}")?;
118 }
119 write!(f, "]")?;
120 }
121 write!(f, "}}")
122 }
123}
124
125#[test]
126fn test_escape() {
127 assert_eq!(escape("a\nb").to_string(), r"a\nb");
128 assert_eq!(escape("C:\\Miette").to_string(), r"C:\\Miette");
129}