1 use std::fmt;
2 
3 use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
4 
5 use crate::diagnostic_chain::DiagnosticChain;
6 use crate::protocol::{Diagnostic, Severity};
7 use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
8 
9 /**
10 [`ReportHandler`] that renders plain text and avoids extraneous graphics.
11 It's optimized for screen readers and braille users, but is also used in any
12 non-graphical environments, such as non-TTY output.
13 */
14 #[derive(Debug, Clone)]
15 pub struct NarratableReportHandler {
16     context_lines: usize,
17     with_cause_chain: bool,
18     footer: Option<String>,
19 }
20 
21 impl NarratableReportHandler {
22     /// Create a new [`NarratableReportHandler`]. There are no customization
23     /// options.
new() -> Self24     pub const fn new() -> Self {
25         Self {
26             footer: None,
27             context_lines: 1,
28             with_cause_chain: true,
29         }
30     }
31 
32     /// Include the cause chain of the top-level error in the report, if
33     /// available.
with_cause_chain(mut self) -> Self34     pub const fn with_cause_chain(mut self) -> Self {
35         self.with_cause_chain = true;
36         self
37     }
38 
39     /// Do not include the cause chain of the top-level error in the report.
without_cause_chain(mut self) -> Self40     pub const fn without_cause_chain(mut self) -> Self {
41         self.with_cause_chain = false;
42         self
43     }
44 
45     /// Set the footer to be displayed at the end of the report.
with_footer(mut self, footer: String) -> Self46     pub fn with_footer(mut self, footer: String) -> Self {
47         self.footer = Some(footer);
48         self
49     }
50 
51     /// Sets the number of lines of context to show around each error.
with_context_lines(mut self, lines: usize) -> Self52     pub const fn with_context_lines(mut self, lines: usize) -> Self {
53         self.context_lines = lines;
54         self
55     }
56 }
57 
58 impl Default for NarratableReportHandler {
default() -> Self59     fn default() -> Self {
60         Self::new()
61     }
62 }
63 
64 impl NarratableReportHandler {
65     /// Render a [`Diagnostic`]. This function is mostly internal and meant to
66     /// be called by the toplevel [`ReportHandler`] handler, but is
67     /// made public to make it easier (possible) to test in isolation from
68     /// global state.
render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result69     pub fn render_report(
70         &self,
71         f: &mut impl fmt::Write,
72         diagnostic: &(dyn Diagnostic),
73     ) -> fmt::Result {
74         self.render_header(f, diagnostic)?;
75         if self.with_cause_chain {
76             self.render_causes(f, diagnostic)?;
77         }
78         let src = diagnostic.source_code();
79         self.render_snippets(f, diagnostic, src)?;
80         self.render_footer(f, diagnostic)?;
81         self.render_related(f, diagnostic, src)?;
82         if let Some(footer) = &self.footer {
83             writeln!(f, "{}", footer)?;
84         }
85         Ok(())
86     }
87 
render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result88     fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
89         writeln!(f, "{}", diagnostic)?;
90         let severity = match diagnostic.severity() {
91             Some(Severity::Error) | None => "error",
92             Some(Severity::Warning) => "warning",
93             Some(Severity::Advice) => "advice",
94         };
95         writeln!(f, "    Diagnostic severity: {}", severity)?;
96         Ok(())
97     }
98 
render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result99     fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
100         if let Some(cause_iter) = diagnostic
101             .diagnostic_source()
102             .map(DiagnosticChain::from_diagnostic)
103             .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
104         {
105             for error in cause_iter {
106                 writeln!(f, "    Caused by: {}", error)?;
107             }
108         }
109 
110         Ok(())
111     }
112 
render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result113     fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
114         if let Some(help) = diagnostic.help() {
115             writeln!(f, "diagnostic help: {}", help)?;
116         }
117         if let Some(code) = diagnostic.code() {
118             writeln!(f, "diagnostic code: {}", code)?;
119         }
120         if let Some(url) = diagnostic.url() {
121             writeln!(f, "For more details, see:\n{}", url)?;
122         }
123         Ok(())
124     }
125 
render_related( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), parent_src: Option<&dyn SourceCode>, ) -> fmt::Result126     fn render_related(
127         &self,
128         f: &mut impl fmt::Write,
129         diagnostic: &(dyn Diagnostic),
130         parent_src: Option<&dyn SourceCode>,
131     ) -> fmt::Result {
132         if let Some(related) = diagnostic.related() {
133             writeln!(f)?;
134             for rel in related {
135                 match rel.severity() {
136                     Some(Severity::Error) | None => write!(f, "Error: ")?,
137                     Some(Severity::Warning) => write!(f, "Warning: ")?,
138                     Some(Severity::Advice) => write!(f, "Advice: ")?,
139                 };
140                 self.render_header(f, rel)?;
141                 writeln!(f)?;
142                 self.render_causes(f, rel)?;
143                 let src = rel.source_code().or(parent_src);
144                 self.render_snippets(f, rel, src)?;
145                 self.render_footer(f, rel)?;
146                 self.render_related(f, rel, src)?;
147             }
148         }
149         Ok(())
150     }
151 
render_snippets( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), source_code: Option<&dyn SourceCode>, ) -> fmt::Result152     fn render_snippets(
153         &self,
154         f: &mut impl fmt::Write,
155         diagnostic: &(dyn Diagnostic),
156         source_code: Option<&dyn SourceCode>,
157     ) -> fmt::Result {
158         if let Some(source) = source_code {
159             if let Some(labels) = diagnostic.labels() {
160                 let mut labels = labels.collect::<Vec<_>>();
161                 labels.sort_unstable_by_key(|l| l.inner().offset());
162                 if !labels.is_empty() {
163                     let contents = labels
164                         .iter()
165                         .map(|label| {
166                             source.read_span(label.inner(), self.context_lines, self.context_lines)
167                         })
168                         .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
169                         .map_err(|_| fmt::Error)?;
170                     let mut contexts = Vec::new();
171                     for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
172                         if contexts.is_empty() {
173                             contexts.push((right, right_conts));
174                         } else {
175                             let (left, left_conts) = contexts.last().unwrap().clone();
176                             let left_end = left.offset() + left.len();
177                             let right_end = right.offset() + right.len();
178                             if left_conts.line() + left_conts.line_count() >= right_conts.line() {
179                                 // The snippets will overlap, so we create one Big Chunky Boi
180                                 let new_span = LabeledSpan::new(
181                                     left.label().map(String::from),
182                                     left.offset(),
183                                     if right_end >= left_end {
184                                         // Right end goes past left end
185                                         right_end - left.offset()
186                                     } else {
187                                         // right is contained inside left
188                                         left.len()
189                                     },
190                                 );
191                                 if source
192                                     .read_span(
193                                         new_span.inner(),
194                                         self.context_lines,
195                                         self.context_lines,
196                                     )
197                                     .is_ok()
198                                 {
199                                     contexts.pop();
200                                     contexts.push((
201                                         new_span, // We'll throw this away later
202                                         left_conts,
203                                     ));
204                                 } else {
205                                     contexts.push((right, right_conts));
206                                 }
207                             } else {
208                                 contexts.push((right, right_conts));
209                             }
210                         }
211                     }
212                     for (ctx, _) in contexts {
213                         self.render_context(f, source, &ctx, &labels[..])?;
214                     }
215                 }
216             }
217         }
218         Ok(())
219     }
220 
render_context( &self, f: &mut impl fmt::Write, source: &dyn SourceCode, context: &LabeledSpan, labels: &[LabeledSpan], ) -> fmt::Result221     fn render_context(
222         &self,
223         f: &mut impl fmt::Write,
224         source: &dyn SourceCode,
225         context: &LabeledSpan,
226         labels: &[LabeledSpan],
227     ) -> fmt::Result {
228         let (contents, lines) = self.get_lines(source, context.inner())?;
229         write!(f, "Begin snippet")?;
230         if let Some(filename) = contents.name() {
231             write!(f, " for {}", filename,)?;
232         }
233         writeln!(
234             f,
235             " starting at line {}, column {}",
236             contents.line() + 1,
237             contents.column() + 1
238         )?;
239         writeln!(f)?;
240         for line in &lines {
241             writeln!(f, "snippet line {}: {}", line.line_number, line.text)?;
242             let relevant = labels
243                 .iter()
244                 .filter_map(|l| line.span_attach(l.inner()).map(|a| (a, l)));
245             for (attach, label) in relevant {
246                 match attach {
247                     SpanAttach::Contained { col_start, col_end } if col_start == col_end => {
248                         write!(
249                             f,
250                             "    label at line {}, column {}",
251                             line.line_number, col_start,
252                         )?;
253                     }
254                     SpanAttach::Contained { col_start, col_end } => {
255                         write!(
256                             f,
257                             "    label at line {}, columns {} to {}",
258                             line.line_number, col_start, col_end,
259                         )?;
260                     }
261                     SpanAttach::Starts { col_start } => {
262                         write!(
263                             f,
264                             "    label starting at line {}, column {}",
265                             line.line_number, col_start,
266                         )?;
267                     }
268                     SpanAttach::Ends { col_end } => {
269                         write!(
270                             f,
271                             "    label ending at line {}, column {}",
272                             line.line_number, col_end,
273                         )?;
274                     }
275                 }
276                 if let Some(label) = label.label() {
277                     write!(f, ": {}", label)?;
278                 }
279                 writeln!(f)?;
280             }
281         }
282         Ok(())
283     }
284 
get_lines<'a>( &'a self, source: &'a dyn SourceCode, context_span: &'a SourceSpan, ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error>285     fn get_lines<'a>(
286         &'a self,
287         source: &'a dyn SourceCode,
288         context_span: &'a SourceSpan,
289     ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
290         let context_data = source
291             .read_span(context_span, self.context_lines, self.context_lines)
292             .map_err(|_| fmt::Error)?;
293         let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
294         let mut line = context_data.line();
295         let mut column = context_data.column();
296         let mut offset = context_data.span().offset();
297         let mut line_offset = offset;
298         let mut iter = context.chars().peekable();
299         let mut line_str = String::new();
300         let mut lines = Vec::new();
301         while let Some(char) = iter.next() {
302             offset += char.len_utf8();
303             let mut at_end_of_file = false;
304             match char {
305                 '\r' => {
306                     if iter.next_if_eq(&'\n').is_some() {
307                         offset += 1;
308                         line += 1;
309                         column = 0;
310                     } else {
311                         line_str.push(char);
312                         column += 1;
313                     }
314                     at_end_of_file = iter.peek().is_none();
315                 }
316                 '\n' => {
317                     at_end_of_file = iter.peek().is_none();
318                     line += 1;
319                     column = 0;
320                 }
321                 _ => {
322                     line_str.push(char);
323                     column += 1;
324                 }
325             }
326 
327             if iter.peek().is_none() && !at_end_of_file {
328                 line += 1;
329             }
330 
331             if column == 0 || iter.peek().is_none() {
332                 lines.push(Line {
333                     line_number: line,
334                     offset: line_offset,
335                     text: line_str.clone(),
336                     at_end_of_file,
337                 });
338                 line_str.clear();
339                 line_offset = offset;
340             }
341         }
342         Ok((context_data, lines))
343     }
344 }
345 
346 impl ReportHandler for NarratableReportHandler {
debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result347     fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
348         if f.alternate() {
349             return fmt::Debug::fmt(diagnostic, f);
350         }
351 
352         self.render_report(f, diagnostic)
353     }
354 }
355 
356 /*
357 Support types
358 */
359 
360 struct Line {
361     line_number: usize,
362     offset: usize,
363     text: String,
364     at_end_of_file: bool,
365 }
366 
367 enum SpanAttach {
368     Contained { col_start: usize, col_end: usize },
369     Starts { col_start: usize },
370     Ends { col_end: usize },
371 }
372 
373 /// Returns column at offset, and nearest boundary if offset is in the middle of
374 /// the character
safe_get_column(text: &str, offset: usize, start: bool) -> usize375 fn safe_get_column(text: &str, offset: usize, start: bool) -> usize {
376     let mut column = text.get(0..offset).map(|s| s.width()).unwrap_or_else(|| {
377         let mut column = 0;
378         for (idx, c) in text.char_indices() {
379             if offset <= idx {
380                 break;
381             }
382             column += c.width().unwrap_or(0);
383         }
384         column
385     });
386     if start {
387         // Offset are zero-based, so plus one
388         column += 1;
389     } // On the other hand for end span, offset refers for the next column
390       // So we should do -1. column+1-1 == column
391     column
392 }
393 
394 impl Line {
span_attach(&self, span: &SourceSpan) -> Option<SpanAttach>395     fn span_attach(&self, span: &SourceSpan) -> Option<SpanAttach> {
396         let span_end = span.offset() + span.len();
397         let line_end = self.offset + self.text.len();
398 
399         let start_after = span.offset() >= self.offset;
400         let end_before = self.at_end_of_file || span_end <= line_end;
401 
402         if start_after && end_before {
403             let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
404             let col_end = if span.is_empty() {
405                 col_start
406             } else {
407                 // span_end refers to the next character after token
408                 // while col_end refers to the exact character, so -1
409                 safe_get_column(&self.text, span_end - self.offset, false)
410             };
411             return Some(SpanAttach::Contained { col_start, col_end });
412         }
413         if start_after && span.offset() <= line_end {
414             let col_start = safe_get_column(&self.text, span.offset() - self.offset, true);
415             return Some(SpanAttach::Starts { col_start });
416         }
417         if end_before && span_end >= self.offset {
418             let col_end = safe_get_column(&self.text, span_end - self.offset, false);
419             return Some(SpanAttach::Ends { col_end });
420         }
421         None
422     }
423 }
424